blob: ef5991e9015dfa8b2c051badaaeec81ee0c4607e [file] [log] [blame]
/*
* 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 <asm/unaligned.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;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 0, 0)
/*
* Free tport via RCU.
*/
static void ft_tport_rcu_free(struct rcu_head *rcu)
{
struct ft_tport *tport = container_of(rcu, struct ft_tport, rcu);
kfree(tport);
}
#endif
/*
* 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;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 0, 0)
kfree_rcu(tport, rcu);
#else
call_rcu(&tport->rcu, ft_tport_rcu_free);
#endif
}
/*
* 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;
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
struct hlist_node *pos;
#endif
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)];
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
hlist_for_each_entry_rcu(sess, pos, head, hash) {
#else
hlist_for_each_entry_rcu(sess, head, hash) {
#endif
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;
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
struct hlist_node *pos;
#endif
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)];
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
hlist_for_each_entry_rcu(sess, pos, head, hash) {
#else
hlist_for_each_entry_rcu(sess, head, hash) {
#endif
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;
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
struct hlist_node *pos;
#endif
struct ft_sess *sess;
head = &tport->hash[ft_sess_hash(port_id)];
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
hlist_for_each_entry_rcu(sess, pos, head, hash) {
#else
hlist_for_each_entry_rcu(sess, head, hash) {
#endif
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];
} __attribute__((__packed__)) *id;
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;
}
/**
* tcm_fcp_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--;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 36) && !defined(RHEL_MAJOR)
static inline u32 fc_frame_sid(const struct fc_frame *fp)
{
return ntoh24(fc_frame_header_get(fp)->fh_s_id);
}
#endif
/*
* Handle incoming FCP request.
* Caller has verified that the frame is type FCP.
* Note that this may be called directly from the softirq context.
*/
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 36) \
&& (!defined(RHEL_MAJOR) || RHEL_MAJOR -0 <= 5)
static void ft_recv(struct fc_lport *lport, struct fc_seq *sp,
struct fc_frame *fp)
#else
static void ft_recv(struct fc_lport *lport, struct fc_frame *fp)
#endif
{
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);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 36) \
&& (!defined(RHEL_MAJOR) || RHEL_MAJOR -0 <= 5)
lport->tt.exch_done(sp);
#endif
/* 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;
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
struct hlist_node *pos;
#endif
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++)
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 9, 0)
hlist_for_each_entry_rcu(sess, pos, head, hash)
#else
hlist_for_each_entry_rcu(sess, head, hash)
#endif
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 == NULL) {
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 == NULL)
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,
};