mirror of
https://github.com/SCST-project/scst.git
synced 2026-05-14 09:11:27 +00:00
574 lines
14 KiB
C
574 lines
14 KiB
C
/*
|
|
* Copyright (c) 2010 Cisco Systems, Inc.
|
|
*
|
|
* This program is free software; you may redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
#include <linux/kernel.h>
|
|
#include <linux/types.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/hash.h>
|
|
#include <scsi/libfc.h>
|
|
#include <scsi/fc/fc_els.h>
|
|
#include "fcst.h"
|
|
|
|
static void ft_sess_put(struct ft_sess *sess);
|
|
|
|
static ssize_t ft_format_wwn(char *buf, size_t len, u64 wwn)
|
|
{
|
|
u8 b[8];
|
|
|
|
put_unaligned_be64(wwn, b);
|
|
return snprintf(buf, len,
|
|
"%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x",
|
|
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]);
|
|
}
|
|
|
|
/*
|
|
* Lookup or allocate target local port.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static struct ft_tport *ft_tport_create(struct fc_lport *lport)
|
|
{
|
|
struct ft_tport *tport;
|
|
char name[FT_NAMELEN];
|
|
int i;
|
|
|
|
ft_format_wwn(name, sizeof(name), lport->wwpn);
|
|
FT_SESS_DBG("create %s\n", name);
|
|
|
|
tport = rcu_dereference_protected((void __force __rcu *)
|
|
lport->prov[FC_TYPE_FCP],
|
|
lockdep_is_held(&ft_lport_lock));
|
|
if (tport) {
|
|
FT_SESS_DBG("tport alloc %s - already setup\n", name);
|
|
return tport;
|
|
}
|
|
|
|
tport = kzalloc(sizeof(*tport), GFP_KERNEL);
|
|
if (!tport) {
|
|
FT_SESS_DBG("tport alloc %s failed\n", name);
|
|
return NULL;
|
|
}
|
|
|
|
tport->tgt = scst_register_target(&ft_scst_template, name);
|
|
if (!tport->tgt) {
|
|
FT_SESS_DBG("register_target %s failed\n", name);
|
|
kfree(tport);
|
|
return NULL;
|
|
}
|
|
scst_tgt_set_tgt_priv(tport->tgt, tport);
|
|
|
|
tport->lport = lport;
|
|
for (i = 0; i < FT_SESS_HASH_SIZE; i++)
|
|
INIT_HLIST_HEAD(&tport->hash[i]);
|
|
|
|
rcu_assign_pointer(*(void __force __rcu **)&lport->prov[FC_TYPE_FCP],
|
|
tport);
|
|
FT_SESS_DBG("register_target %s succeeded\n", name);
|
|
return tport;
|
|
}
|
|
|
|
/*
|
|
* Delete target local port, if any, associated with the local port.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static void ft_tport_delete(struct ft_tport *tport)
|
|
{
|
|
struct fc_lport *lport;
|
|
struct scst_tgt *tgt;
|
|
|
|
tgt = tport->tgt;
|
|
BUG_ON(!tgt);
|
|
FT_SESS_DBG("delete %s\n", scst_get_tgt_name(tgt));
|
|
scst_unregister_target(tgt);
|
|
lport = tport->lport;
|
|
BUG_ON(tport != lport->prov[FC_TYPE_FCP]);
|
|
rcu_assign_pointer(*(void __force __rcu **)&lport->prov[FC_TYPE_FCP],
|
|
NULL);
|
|
tport->lport = NULL;
|
|
kfree_rcu(tport, rcu);
|
|
}
|
|
|
|
/*
|
|
* Add local port.
|
|
* Called thru fc_lport_iterate().
|
|
*/
|
|
void ft_lport_add(struct fc_lport *lport, void *arg)
|
|
{
|
|
mutex_lock(&ft_lport_lock);
|
|
ft_tport_create(lport);
|
|
mutex_unlock(&ft_lport_lock);
|
|
}
|
|
|
|
/*
|
|
* Delete local port.
|
|
* Called thru fc_lport_iterate().
|
|
*/
|
|
void ft_lport_del(struct fc_lport *lport, void *arg)
|
|
{
|
|
struct ft_tport *tport;
|
|
|
|
mutex_lock(&ft_lport_lock);
|
|
tport = lport->prov[FC_TYPE_FCP];
|
|
if (tport)
|
|
ft_tport_delete(tport);
|
|
mutex_unlock(&ft_lport_lock);
|
|
}
|
|
|
|
/*
|
|
* Notification of local port change from libfc.
|
|
* Create or delete local port and associated tport.
|
|
*/
|
|
int ft_lport_notify(struct notifier_block *nb, unsigned long event, void *arg)
|
|
{
|
|
struct fc_lport *lport = arg;
|
|
|
|
switch (event) {
|
|
case FC_LPORT_EV_ADD:
|
|
ft_lport_add(lport, NULL);
|
|
break;
|
|
case FC_LPORT_EV_DEL:
|
|
ft_lport_del(lport, NULL);
|
|
break;
|
|
}
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
/*
|
|
* Hash function for FC_IDs.
|
|
*/
|
|
static u32 ft_sess_hash(u32 port_id)
|
|
{
|
|
return hash_32(port_id, FT_SESS_HASH_BITS);
|
|
}
|
|
|
|
/*
|
|
* Find session in local port.
|
|
* Sessions and hash lists are RCU-protected.
|
|
* A reference is taken which must be eventually freed.
|
|
*/
|
|
static struct ft_sess *ft_sess_get(struct fc_lport *lport, u32 port_id)
|
|
{
|
|
struct ft_tport *tport;
|
|
struct hlist_head *head;
|
|
struct ft_sess *sess;
|
|
|
|
rcu_read_lock();
|
|
tport = rcu_dereference_protected((void __force __rcu *)
|
|
lport->prov[FC_TYPE_FCP], true);
|
|
if (!tport)
|
|
goto out;
|
|
|
|
head = &tport->hash[ft_sess_hash(port_id)];
|
|
hlist_for_each_entry_rcu(sess, head, hash) {
|
|
if (sess->port_id == port_id) {
|
|
if (!kref_get_unless_zero(&sess->kref))
|
|
sess = NULL;
|
|
rcu_read_unlock();
|
|
FT_SESS_DBG("port_id %x found %p\n", port_id, sess);
|
|
return sess;
|
|
}
|
|
}
|
|
out:
|
|
rcu_read_unlock();
|
|
FT_SESS_DBG("port_id %x not found\n", port_id);
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Allocate session and enter it in the hash for the local port.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static int ft_sess_create(struct ft_tport *tport, struct fc_rport_priv *rdata,
|
|
u32 fcp_parm)
|
|
{
|
|
struct ft_sess *sess;
|
|
struct scst_session *scst_sess;
|
|
struct hlist_head *head;
|
|
u32 port_id;
|
|
char name[FT_NAMELEN];
|
|
|
|
port_id = rdata->ids.port_id;
|
|
if (!rdata->maxframe_size) {
|
|
FT_SESS_DBG("port_id %x maxframe_size 0\n", port_id);
|
|
return FC_SPP_RESP_CONF;
|
|
}
|
|
|
|
head = &tport->hash[ft_sess_hash(port_id)];
|
|
hlist_for_each_entry_rcu(sess, head, hash) {
|
|
if (sess->port_id == port_id) {
|
|
sess->params = fcp_parm;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sess = kzalloc(sizeof(*sess), GFP_KERNEL);
|
|
if (!sess)
|
|
return FC_SPP_RESP_RES; /* out of resources */
|
|
|
|
sess->port_name = rdata->ids.port_name;
|
|
sess->max_payload = rdata->maxframe_size;
|
|
sess->max_lso_payload = rdata->maxframe_size;
|
|
if (tport->lport->seq_offload)
|
|
sess->max_lso_payload = tport->lport->lso_max;
|
|
sess->params = fcp_parm;
|
|
sess->tport = tport;
|
|
sess->port_id = port_id;
|
|
kref_init(&sess->kref); /* ref for table entry */
|
|
|
|
ft_format_wwn(name, sizeof(name), rdata->ids.port_name);
|
|
FT_SESS_DBG("register %s\n", name);
|
|
scst_sess = scst_register_session(tport->tgt, 0, name, sess, NULL,
|
|
NULL);
|
|
if (!scst_sess) {
|
|
kfree(sess);
|
|
return FC_SPP_RESP_RES; /* out of resources */
|
|
}
|
|
sess->scst_sess = scst_sess;
|
|
hlist_add_head_rcu(&sess->hash, head);
|
|
tport->sess_count++;
|
|
|
|
FT_SESS_DBG("port_id %x sess %p\n", port_id, sess);
|
|
|
|
rdata->prli_count++;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Unhash the session.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static void ft_sess_unhash(struct ft_sess *sess)
|
|
{
|
|
struct ft_tport *tport = sess->tport;
|
|
|
|
hlist_del_rcu(&sess->hash);
|
|
BUG_ON(!tport->sess_count);
|
|
tport->sess_count--;
|
|
sess->port_id = -1;
|
|
sess->params = 0;
|
|
}
|
|
|
|
/*
|
|
* Delete session from hash.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static struct ft_sess *ft_sess_delete(struct ft_tport *tport, u32 port_id)
|
|
{
|
|
struct hlist_head *head;
|
|
struct ft_sess *sess;
|
|
|
|
head = &tport->hash[ft_sess_hash(port_id)];
|
|
hlist_for_each_entry_rcu(sess, head, hash) {
|
|
if (sess->port_id == port_id) {
|
|
ft_sess_unhash(sess);
|
|
return sess;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Remove session and send PRLO.
|
|
* This is called when the target is being deleted.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static void ft_sess_close(struct ft_sess *sess)
|
|
{
|
|
u32 port_id;
|
|
|
|
port_id = sess->port_id;
|
|
if (port_id == -1)
|
|
return;
|
|
FT_SESS_DBG("port_id %x\n", port_id);
|
|
ft_sess_unhash(sess);
|
|
ft_sess_put(sess);
|
|
/* XXX should send LOGO or PRLO to rport */
|
|
}
|
|
|
|
/*
|
|
* Allocate and fill in the SPC Transport ID for persistent reservations.
|
|
*/
|
|
int ft_get_transport_id(struct scst_tgt *tgt, struct scst_session *scst_sess, uint8_t **result)
|
|
{
|
|
struct ft_sess *sess;
|
|
struct {
|
|
u8 format_proto; /* format and protocol ID (0 for FC) */
|
|
u8 __resv1[7];
|
|
__be64 port_name; /* N_Port Name */
|
|
u8 __resv2[8];
|
|
} *id;
|
|
|
|
BUILD_BUG_ON(sizeof(*id) != 24);
|
|
|
|
if (!scst_sess)
|
|
return SCSI_TRANSPORTID_PROTOCOLID_FCP2;
|
|
|
|
id = kzalloc(sizeof(*id), GFP_KERNEL);
|
|
if (!id)
|
|
return -ENOMEM;
|
|
|
|
sess = scst_sess_get_tgt_priv(scst_sess);
|
|
id->port_name = cpu_to_be64(sess->port_name);
|
|
id->format_proto = SCSI_TRANSPORTID_PROTOCOLID_FCP2;
|
|
*result = (uint8_t *)id;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* libfc ops involving sessions.
|
|
*/
|
|
|
|
/*
|
|
* Handle PRLI (process login) request.
|
|
* This could be a PRLI we're sending or receiving.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
static int ft_prli_locked(struct fc_rport_priv *rdata, u32 spp_len,
|
|
const struct fc_els_spp *rspp, struct fc_els_spp *spp)
|
|
{
|
|
struct ft_tport *tport;
|
|
u32 fcp_parm;
|
|
int ret;
|
|
|
|
if (!rspp)
|
|
goto fill;
|
|
|
|
if (rspp->spp_flags & (FC_SPP_OPA_VAL | FC_SPP_RPA_VAL))
|
|
return FC_SPP_RESP_NO_PA;
|
|
|
|
/*
|
|
* If both target and initiator bits are off, the SPP is invalid.
|
|
*/
|
|
fcp_parm = ntohl(rspp->spp_params);
|
|
if (!(fcp_parm & (FCP_SPPF_INIT_FCN | FCP_SPPF_TARG_FCN)))
|
|
return FC_SPP_RESP_INVL;
|
|
|
|
/*
|
|
* Create session (image pair) only if requested by
|
|
* EST_IMG_PAIR flag and if the requestor is an initiator.
|
|
*/
|
|
if (rspp->spp_flags & FC_SPP_EST_IMG_PAIR) {
|
|
spp->spp_flags |= FC_SPP_EST_IMG_PAIR;
|
|
|
|
if (!(fcp_parm & FCP_SPPF_INIT_FCN))
|
|
return FC_SPP_RESP_CONF;
|
|
tport = rcu_dereference_protected((void __force __rcu *)
|
|
rdata->local_port->prov[FC_TYPE_FCP],
|
|
lockdep_is_held(&ft_lport_lock));
|
|
if (!tport) {
|
|
/* not a target for this local port */
|
|
return FC_SPP_RESP_CONF;
|
|
}
|
|
if (!tport->enabled) {
|
|
pr_err("Refused login from %#x because target port %s not yet enabled",
|
|
rdata->ids.port_id,
|
|
tport->tgt->tgt_name);
|
|
return FC_SPP_RESP_CONF;
|
|
}
|
|
ret = ft_sess_create(tport, rdata, fcp_parm);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* OR in our service parameters with other provider (initiator), if any.
|
|
* If the initiator indicates RETRY, we must support that, too.
|
|
* Don't force RETRY on the initiator, though.
|
|
*/
|
|
fill:
|
|
fcp_parm = ntohl(spp->spp_params);
|
|
spp->spp_params = htonl(fcp_parm | FCP_SPPF_TARG_FCN);
|
|
return FC_SPP_RESP_ACK;
|
|
}
|
|
|
|
/**
|
|
* ft_prli() - Handle incoming or outgoing PRLI for the FCP target
|
|
* @rdata: remote port private
|
|
* @spp_len: service parameter page length
|
|
* @rspp: received service parameter page (NULL for outgoing PRLI)
|
|
* @spp: response service parameter page
|
|
*
|
|
* Returns spp response code.
|
|
*/
|
|
static int ft_prli(struct fc_rport_priv *rdata, u32 spp_len,
|
|
const struct fc_els_spp *rspp, struct fc_els_spp *spp)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&ft_lport_lock);
|
|
ret = ft_prli_locked(rdata, spp_len, rspp, spp);
|
|
mutex_unlock(&ft_lport_lock);
|
|
FT_SESS_DBG("port_id %x flags %x ret %x\n",
|
|
rdata->ids.port_id, rspp ? rspp->spp_flags : 0, ret);
|
|
return ret;
|
|
}
|
|
|
|
static void ft_sess_rcu_free(struct rcu_head *rcu)
|
|
{
|
|
struct ft_sess *sess = container_of(rcu, struct ft_sess, rcu);
|
|
|
|
kfree(sess);
|
|
}
|
|
|
|
static void ft_sess_free(struct kref *kref)
|
|
{
|
|
struct ft_sess *sess = container_of(kref, struct ft_sess, kref);
|
|
struct scst_session *scst_sess;
|
|
|
|
scst_sess = sess->scst_sess;
|
|
FT_SESS_DBG("unregister %s\n", scst_sess->initiator_name);
|
|
scst_unregister_session(scst_sess, 0, NULL);
|
|
call_rcu(&sess->rcu, ft_sess_rcu_free);
|
|
}
|
|
|
|
static void ft_sess_put(struct ft_sess *sess)
|
|
{
|
|
BUG_ON(!sess);
|
|
BUG_ON(kref_read(&sess->kref) <= 0);
|
|
kref_put(&sess->kref, ft_sess_free);
|
|
}
|
|
|
|
static void ft_prlo(struct fc_rport_priv *rdata)
|
|
{
|
|
struct ft_sess *sess;
|
|
struct ft_tport *tport;
|
|
|
|
mutex_lock(&ft_lport_lock);
|
|
tport = rcu_dereference_protected((void __force __rcu *)
|
|
rdata->local_port->prov[FC_TYPE_FCP],
|
|
lockdep_is_held(&ft_lport_lock));
|
|
if (!tport) {
|
|
mutex_unlock(&ft_lport_lock);
|
|
return;
|
|
}
|
|
sess = ft_sess_delete(tport, rdata->ids.port_id);
|
|
if (!sess) {
|
|
mutex_unlock(&ft_lport_lock);
|
|
return;
|
|
}
|
|
mutex_unlock(&ft_lport_lock);
|
|
|
|
ft_sess_put(sess); /* release from table */
|
|
rdata->prli_count--;
|
|
}
|
|
|
|
/*
|
|
* Handle incoming FCP request.
|
|
* Caller has verified that the frame is type FCP.
|
|
* Note that this may be called directly from the softirq context.
|
|
*/
|
|
static void ft_recv(struct fc_lport *lport, struct fc_frame *fp)
|
|
{
|
|
struct ft_sess *sess;
|
|
u32 sid = fc_frame_sid(fp);
|
|
|
|
FT_SESS_DBG("sid %x preempt %x\n", sid, preempt_count());
|
|
|
|
sess = ft_sess_get(lport, sid);
|
|
if (!sess) {
|
|
FT_SESS_DBG("sid %x sess lookup failed\n", sid);
|
|
/* TBD XXX - if FCP_CMND, send LOGO */
|
|
fc_frame_free(fp);
|
|
return;
|
|
}
|
|
FT_SESS_DBG("sid %x sess lookup returned %p preempt %x\n",
|
|
sid, sess, preempt_count());
|
|
ft_recv_req(sess, fp);
|
|
ft_sess_put(sess);
|
|
}
|
|
|
|
/*
|
|
* Release all sessions for a target.
|
|
* Called through scst_unregister_target() as well as directly.
|
|
* Caller holds ft_lport_lock.
|
|
*/
|
|
int ft_tgt_release(struct scst_tgt *tgt)
|
|
{
|
|
struct ft_tport *tport;
|
|
struct hlist_head *head;
|
|
struct ft_sess *sess;
|
|
|
|
tport = scst_tgt_get_tgt_priv(tgt);
|
|
tport->enabled = 0;
|
|
tport->lport->service_params &= ~FCP_SPPF_TARG_FCN;
|
|
|
|
for (head = tport->hash; head < &tport->hash[FT_SESS_HASH_SIZE]; head++)
|
|
hlist_for_each_entry_rcu(sess, head, hash)
|
|
ft_sess_close(sess);
|
|
|
|
synchronize_rcu();
|
|
return 0;
|
|
}
|
|
|
|
/* Caller must hold a reference on tgt->tgt_kobj. */
|
|
int ft_tgt_enable(struct scst_tgt *tgt, bool enable)
|
|
{
|
|
struct ft_tport *tport;
|
|
int ret = 0;
|
|
|
|
if (enable) {
|
|
FT_SESS_DBG("enable tgt %s\n", tgt->tgt_name);
|
|
tport = scst_tgt_get_tgt_priv(tgt);
|
|
if (!tport) {
|
|
ret = -E_TGT_PRIV_NOT_YET_SET;
|
|
goto out;
|
|
}
|
|
tport->enabled = 1;
|
|
tport->lport->service_params |= FCP_SPPF_TARG_FCN;
|
|
} else {
|
|
FT_SESS_DBG("disable tgt %s\n", tgt->tgt_name);
|
|
ft_tgt_release(tgt);
|
|
}
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
bool ft_tgt_enabled(struct scst_tgt *tgt)
|
|
{
|
|
struct ft_tport *tport;
|
|
|
|
if (!tgt)
|
|
return false;
|
|
|
|
tport = scst_tgt_get_tgt_priv(tgt);
|
|
return tport->enabled;
|
|
}
|
|
|
|
/*
|
|
* Report AEN (Asynchronous Event Notification) from device to initiator.
|
|
* See notes in scst.h.
|
|
*/
|
|
int ft_report_aen(struct scst_aen *aen)
|
|
{
|
|
struct ft_sess *sess;
|
|
|
|
sess = scst_sess_get_tgt_priv(scst_aen_get_sess(aen));
|
|
FT_SESS_DBG("AEN event %d sess to %x lun %lld\n",
|
|
aen->event_fn, sess->port_id, scst_aen_get_lun(aen));
|
|
return SCST_AEN_RES_FAILED; /* XXX TBD */
|
|
}
|
|
|
|
/*
|
|
* Provider ops for libfc.
|
|
*/
|
|
struct fc4_prov ft_prov = {
|
|
.prli = ft_prli,
|
|
.prlo = ft_prlo,
|
|
.recv = ft_recv,
|
|
.module = THIS_MODULE,
|
|
};
|