Commit 7a291083 authored by Jan-Bernd Themann's avatar Jan-Bernd Themann Committed by Jeff Garzik

[PATCH] ehea: IBM eHEA Ethernet Device Driver

Hi Jeff,

I fixed the __iomem issue and tested the driver with sparse. Looks good so far.
Thanks for your effort.

Jan-Bernd Themann
Signed-off-by: default avatarJan-Bernd Themann <themann@de.ibm.com>

 drivers/net/Kconfig             |    9
 drivers/net/Makefile            |    1
 drivers/net/ehea/Makefile       |    6
 drivers/net/ehea/ehea.h         |  447 ++++++
 drivers/net/ehea/ehea_ethtool.c |  294 ++++
 drivers/net/ehea/ehea_hcall.h   |   51
 drivers/net/ehea/ehea_hw.h      |  287 ++++
 drivers/net/ehea/ehea_main.c    | 2654 ++++++++++++++++++++++++++++++++++++++++
 drivers/net/ehea/ehea_phyp.c    |  705 ++++++++++
 drivers/net/ehea/ehea_phyp.h    |  455 ++++++
 drivers/net/ehea/ehea_qmr.c     |  582 ++++++++
 drivers/net/ehea/ehea_qmr.h     |  358 +++++
 12 files changed, 5849 insertions(+)
Signed-off-by: default avatarJeff Garzik <jeff@garzik.org>
parent 7de745e5
...@@ -2360,6 +2360,15 @@ config CHELSIO_T1 ...@@ -2360,6 +2360,15 @@ config CHELSIO_T1
To compile this driver as a module, choose M here: the module To compile this driver as a module, choose M here: the module
will be called cxgb. will be called cxgb.
config EHEA
tristate "eHEA Ethernet support"
depends on IBMEBUS
---help---
This driver supports the IBM pSeries eHEA ethernet adapter.
To compile the driver as a module, choose M here. The module
will be called ehea.
config IXGB config IXGB
tristate "Intel(R) PRO/10GbE support" tristate "Intel(R) PRO/10GbE support"
depends on PCI depends on PCI
......
...@@ -6,6 +6,7 @@ obj-$(CONFIG_E1000) += e1000/ ...@@ -6,6 +6,7 @@ obj-$(CONFIG_E1000) += e1000/
obj-$(CONFIG_IBM_EMAC) += ibm_emac/ obj-$(CONFIG_IBM_EMAC) += ibm_emac/
obj-$(CONFIG_IXGB) += ixgb/ obj-$(CONFIG_IXGB) += ixgb/
obj-$(CONFIG_CHELSIO_T1) += chelsio/ obj-$(CONFIG_CHELSIO_T1) += chelsio/
obj-$(CONFIG_EHEA) += ehea/
obj-$(CONFIG_BONDING) += bonding/ obj-$(CONFIG_BONDING) += bonding/
obj-$(CONFIG_GIANFAR) += gianfar_driver.o obj-$(CONFIG_GIANFAR) += gianfar_driver.o
......
#
# Makefile for the eHEA ethernet device driver for IBM eServer System p
#
ehea-y = ehea_main.o ehea_phyp.o ehea_qmr.o ehea_ethtool.o ehea_phyp.o
obj-$(CONFIG_EHEA) += ehea.o
This diff is collapsed.
/*
* linux/drivers/net/ehea/ehea_ethtool.c
*
* eHEA ethernet device driver for IBM eServer System p
*
* (C) Copyright IBM Corp. 2006
*
* Authors:
* Christoph Raisch <raisch@de.ibm.com>
* Jan-Bernd Themann <themann@de.ibm.com>
* Thomas Klein <tklein@de.ibm.com>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "ehea.h"
#include "ehea_phyp.h"
static int ehea_get_settings(struct net_device *dev, struct ethtool_cmd *cmd)
{
struct ehea_port *port = netdev_priv(dev);
int ret;
ret = ehea_sense_port_attr(port);
if (ret)
return ret;
if (netif_carrier_ok(dev)) {
switch(port->port_speed) {
case EHEA_SPEED_10M: cmd->speed = SPEED_10; break;
case EHEA_SPEED_100M: cmd->speed = SPEED_100; break;
case EHEA_SPEED_1G: cmd->speed = SPEED_1000; break;
case EHEA_SPEED_10G: cmd->speed = SPEED_10000; break;
}
cmd->duplex = port->full_duplex == 1 ?
DUPLEX_FULL : DUPLEX_HALF;
} else {
cmd->speed = -1;
cmd->duplex = -1;
}
cmd->supported = (SUPPORTED_10000baseT_Full | SUPPORTED_1000baseT_Full
| SUPPORTED_100baseT_Full | SUPPORTED_100baseT_Half
| SUPPORTED_10baseT_Full | SUPPORTED_10baseT_Half
| SUPPORTED_Autoneg | SUPPORTED_FIBRE);
cmd->advertising = (ADVERTISED_10000baseT_Full | ADVERTISED_Autoneg
| ADVERTISED_FIBRE);
cmd->port = PORT_FIBRE;
cmd->autoneg = port->autoneg == 1 ? AUTONEG_ENABLE : AUTONEG_DISABLE;
return 0;
}
static int ehea_set_settings(struct net_device *dev, struct ethtool_cmd *cmd)
{
struct ehea_port *port = netdev_priv(dev);
int ret = 0;
u32 sp;
if (cmd->autoneg == AUTONEG_ENABLE) {
sp = EHEA_SPEED_AUTONEG;
goto doit;
}
switch(cmd->speed) {
case SPEED_10:
if (cmd->duplex == DUPLEX_FULL)
sp = H_SPEED_10M_F;
else
sp = H_SPEED_10M_H;
break;
case SPEED_100:
if (cmd->duplex == DUPLEX_FULL)
sp = H_SPEED_100M_F;
else
sp = H_SPEED_100M_H;
break;
case SPEED_1000:
if (cmd->duplex == DUPLEX_FULL)
sp = H_SPEED_1G_F;
else
ret = -EINVAL;
break;
case SPEED_10000:
if (cmd->duplex == DUPLEX_FULL)
sp = H_SPEED_10G_F;
else
ret = -EINVAL;
break;
default:
ret = -EINVAL;
break;
}
if (ret)
goto out;
doit:
ret = ehea_set_portspeed(port, sp);
if (!ret)
ehea_info("%s: Port speed succesfully set: %dMbps "
"%s Duplex",
port->netdev->name, port->port_speed,
port->full_duplex == 1 ? "Full" : "Half");
out:
return ret;
}
static int ehea_nway_reset(struct net_device *dev)
{
struct ehea_port *port = netdev_priv(dev);
int ret;
ret = ehea_set_portspeed(port, EHEA_SPEED_AUTONEG);
if (!ret)
ehea_info("%s: Port speed succesfully set: %dMbps "
"%s Duplex",
port->netdev->name, port->port_speed,
port->full_duplex == 1 ? "Full" : "Half");
return ret;
}
static void ehea_get_drvinfo(struct net_device *dev,
struct ethtool_drvinfo *info)
{
strlcpy(info->driver, DRV_NAME, sizeof(info->driver) - 1);
strlcpy(info->version, DRV_VERSION, sizeof(info->version) - 1);
}
static u32 ehea_get_msglevel(struct net_device *dev)
{
struct ehea_port *port = netdev_priv(dev);
return port->msg_enable;
}
static void ehea_set_msglevel(struct net_device *dev, u32 value)
{
struct ehea_port *port = netdev_priv(dev);
port->msg_enable = value;
}
static u32 ehea_get_rx_csum(struct net_device *dev)
{
return 1;
}
static char ehea_ethtool_stats_keys[][ETH_GSTRING_LEN] = {
{"poll_max_processed"},
{"queue_stopped"},
{"min_swqe_avail"},
{"poll_receive_err"},
{"pkt_send"},
{"pkt_xmit"},
{"send_tasklet"},
{"ehea_poll"},
{"nwqe"},
{"swqe_available_0"},
{"sig_comp_iv"},
{"swqe_refill_th"},
{"port resets"},
{"rxo"},
{"rx64"},
{"rx65"},
{"rx128"},
{"rx256"},
{"rx512"},
{"rx1024"},
{"txo"},
{"tx64"},
{"tx65"},
{"tx128"},
{"tx256"},
{"tx512"},
{"tx1024"},
};
static void ehea_get_strings(struct net_device *dev, u32 stringset, u8 *data)
{
if (stringset == ETH_SS_STATS) {
memcpy(data, &ehea_ethtool_stats_keys,
sizeof(ehea_ethtool_stats_keys));
}
}
static int ehea_get_stats_count(struct net_device *dev)
{
return ARRAY_SIZE(ehea_ethtool_stats_keys);
}
static void ehea_get_ethtool_stats(struct net_device *dev,
struct ethtool_stats *stats, u64 *data)
{
u64 hret;
int i;
struct ehea_port *port = netdev_priv(dev);
struct ehea_adapter *adapter = port->adapter;
struct ehea_port_res *pr = &port->port_res[0];
struct port_state *p_state = &pr->p_state;
struct hcp_ehea_port_cb6 *cb6;
for (i = 0; i < ehea_get_stats_count(dev); i++)
data[i] = 0;
i = 0;
data[i++] = p_state->poll_max_processed;
data[i++] = p_state->queue_stopped;
data[i++] = p_state->min_swqe_avail;
data[i++] = p_state->poll_receive_errors;
data[i++] = p_state->pkt_send;
data[i++] = p_state->pkt_xmit;
data[i++] = p_state->send_tasklet;
data[i++] = p_state->ehea_poll;
data[i++] = p_state->nwqe;
data[i++] = atomic_read(&port->port_res[0].swqe_avail);
data[i++] = port->sig_comp_iv;
data[i++] = port->port_res[0].swqe_refill_th;
data[i++] = port->resets;
cb6 = kzalloc(H_CB_ALIGNMENT, GFP_KERNEL);
if (!cb6) {
ehea_error("no mem for cb6");
return;
}
hret = ehea_h_query_ehea_port(adapter->handle, port->logical_port_id,
H_PORT_CB6, H_PORT_CB6_ALL, cb6);
if (netif_msg_hw(port))
ehea_dump(cb6, sizeof(*cb6), "ehea_get_ethtool_stats");
if (hret == H_SUCCESS) {
data[i++] = cb6->rxo;
data[i++] = cb6->rx64;
data[i++] = cb6->rx65;
data[i++] = cb6->rx128;
data[i++] = cb6->rx256;
data[i++] = cb6->rx512;
data[i++] = cb6->rx1024;
data[i++] = cb6->txo;
data[i++] = cb6->tx64;
data[i++] = cb6->tx65;
data[i++] = cb6->tx128;
data[i++] = cb6->tx256;
data[i++] = cb6->tx512;
data[i++] = cb6->tx1024;
} else
ehea_error("query_ehea_port failed");
kfree(cb6);
}
struct ethtool_ops ehea_ethtool_ops = {
.get_settings = ehea_get_settings,
.get_drvinfo = ehea_get_drvinfo,
.get_msglevel = ehea_get_msglevel,
.set_msglevel = ehea_set_msglevel,
.get_link = ethtool_op_get_link,
.get_tx_csum = ethtool_op_get_tx_csum,
.get_sg = ethtool_op_get_sg,
.get_tso = ethtool_op_get_tso,
.set_tso = ethtool_op_set_tso,
.get_strings = ehea_get_strings,
.get_stats_count = ehea_get_stats_count,
.get_ethtool_stats = ehea_get_ethtool_stats,
.get_rx_csum = ehea_get_rx_csum,
.set_settings = ehea_set_settings,
.nway_reset = ehea_nway_reset, /* Restart autonegotiation */
};
void ehea_set_ethtool_ops(struct net_device *netdev)
{
SET_ETHTOOL_OPS(netdev, &ehea_ethtool_ops);
}
/*
* linux/drivers/net/ehea/ehea_hcall.h
*
* eHEA ethernet device driver for IBM eServer System p
*
* (C) Copyright IBM Corp. 2006
*
* Authors:
* Christoph Raisch <raisch@de.ibm.com>
* Jan-Bernd Themann <themann@de.ibm.com>
* Thomas Klein <tklein@de.ibm.com>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef __EHEA_HCALL_H__
#define __EHEA_HCALL_H__
/**
* This file contains HCALL defines that are to be included in the appropriate
* kernel files later
*/
#define H_ALLOC_HEA_RESOURCE 0x278
#define H_MODIFY_HEA_QP 0x250
#define H_QUERY_HEA_QP 0x254
#define H_QUERY_HEA 0x258
#define H_QUERY_HEA_PORT 0x25C
#define H_MODIFY_HEA_PORT 0x260
#define H_REG_BCMC 0x264
#define H_DEREG_BCMC 0x268
#define H_REGISTER_HEA_RPAGES 0x26C
#define H_DISABLE_AND_GET_HEA 0x270
#define H_GET_HEA_INFO 0x274
#define H_ADD_CONN 0x284
#define H_DEL_CONN 0x288
#endif /* __EHEA_HCALL_H__ */
/*
* linux/drivers/net/ehea/ehea_hw.h
*
* eHEA ethernet device driver for IBM eServer System p
*
* (C) Copyright IBM Corp. 2006
*
* Authors:
* Christoph Raisch <raisch@de.ibm.com>
* Jan-Bernd Themann <themann@de.ibm.com>
* Thomas Klein <tklein@de.ibm.com>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef __EHEA_HW_H__
#define __EHEA_HW_H__
#define QPX_SQA_VALUE EHEA_BMASK_IBM(48,63)
#define QPX_RQ1A_VALUE EHEA_BMASK_IBM(48,63)
#define QPX_RQ2A_VALUE EHEA_BMASK_IBM(48,63)
#define QPX_RQ3A_VALUE EHEA_BMASK_IBM(48,63)
#define QPTEMM_OFFSET(x) offsetof(struct ehea_qptemm, x)
struct ehea_qptemm {
u64 qpx_hcr;
u64 qpx_c;
u64 qpx_herr;
u64 qpx_aer;
u64 qpx_sqa;
u64 qpx_sqc;
u64 qpx_rq1a;
u64 qpx_rq1c;
u64 qpx_st;
u64 qpx_aerr;
u64 qpx_tenure;
u64 qpx_reserved1[(0x098 - 0x058) / 8];
u64 qpx_portp;
u64 qpx_reserved2[(0x100 - 0x0A0) / 8];
u64 qpx_t;
u64 qpx_sqhp;
u64 qpx_sqptp;
u64 qpx_reserved3[(0x140 - 0x118) / 8];
u64 qpx_sqwsize;
u64 qpx_reserved4[(0x170 - 0x148) / 8];
u64 qpx_sqsize;
u64 qpx_reserved5[(0x1B0 - 0x178) / 8];
u64 qpx_sigt;
u64 qpx_wqecnt;
u64 qpx_rq1hp;
u64 qpx_rq1ptp;
u64 qpx_rq1size;
u64 qpx_reserved6[(0x220 - 0x1D8) / 8];
u64 qpx_rq1wsize;
u64 qpx_reserved7[(0x240 - 0x228) / 8];
u64 qpx_pd;
u64 qpx_scqn;
u64 qpx_rcqn;
u64 qpx_aeqn;
u64 reserved49;
u64 qpx_ram;
u64 qpx_reserved8[(0x300 - 0x270) / 8];
u64 qpx_rq2a;
u64 qpx_rq2c;
u64 qpx_rq2hp;
u64 qpx_rq2ptp;
u64 qpx_rq2size;
u64 qpx_rq2wsize;
u64 qpx_rq2th;
u64 qpx_rq3a;
u64 qpx_rq3c;
u64 qpx_rq3hp;
u64 qpx_rq3ptp;
u64 qpx_rq3size;
u64 qpx_rq3wsize;
u64 qpx_rq3th;
u64 qpx_lpn;
u64 qpx_reserved9[(0x400 - 0x378) / 8];
u64 reserved_ext[(0x500 - 0x400) / 8];
u64 reserved2[(0x1000 - 0x500) / 8];
};
#define MRx_HCR_LPARID_VALID EHEA_BMASK_IBM(0, 0)
#define MRMWMM_OFFSET(x) offsetof(struct ehea_mrmwmm, x)
struct ehea_mrmwmm {
u64 mrx_hcr;
u64 mrx_c;
u64 mrx_herr;
u64 mrx_aer;
u64 mrx_pp;
u64 reserved1;
u64 reserved2;
u64 reserved3;
u64 reserved4[(0x200 - 0x40) / 8];
u64 mrx_ctl[64];
};
#define QPEDMM_OFFSET(x) offsetof(struct ehea_qpedmm, x)
struct ehea_qpedmm {
u64 reserved0[(0x400) / 8];
u64 qpedx_phh;
u64 qpedx_ppsgp;
u64 qpedx_ppsgu;
u64 qpedx_ppdgp;
u64 qpedx_ppdgu;
u64 qpedx_aph;
u64 qpedx_apsgp;
u64 qpedx_apsgu;
u64 qpedx_apdgp;
u64 qpedx_apdgu;
u64 qpedx_apav;
u64 qpedx_apsav;
u64 qpedx_hcr;
u64 reserved1[4];
u64 qpedx_rrl0;
u64 qpedx_rrrkey0;
u64 qpedx_rrva0;
u64 reserved2;
u64 qpedx_rrl1;
u64 qpedx_rrrkey1;
u64 qpedx_rrva1;
u64 reserved3;
u64 qpedx_rrl2;
u64 qpedx_rrrkey2;
u64 qpedx_rrva2;
u64 reserved4;
u64 qpedx_rrl3;
u64 qpedx_rrrkey3;
u64 qpedx_rrva3;
};
#define CQX_FECADDER EHEA_BMASK_IBM(32, 63)
#define CQX_FEC_CQE_CNT EHEA_BMASK_IBM(32, 63)
#define CQX_N1_GENERATE_COMP_EVENT EHEA_BMASK_IBM(0, 0)
#define CQX_EP_EVENT_PENDING EHEA_BMASK_IBM(0, 0)
#define CQTEMM_OFFSET(x) offsetof(struct ehea_cqtemm, x)
struct ehea_cqtemm {
u64 cqx_hcr;
u64 cqx_c;
u64 cqx_herr;
u64 cqx_aer;
u64 cqx_ptp;
u64 cqx_tp;
u64 cqx_fec;
u64 cqx_feca;
u64 cqx_ep;
u64 cqx_eq;
u64 reserved1;
u64 cqx_n0;
u64 cqx_n1;
u64 reserved2[(0x1000 - 0x60) / 8];
};
#define EQTEMM_OFFSET(x) offsetof(struct ehea_eqtemm, x)
struct ehea_eqtemm {
u64 eqx_hcr;
u64 eqx_c;
u64 eqx_herr;
u64 eqx_aer;
u64 eqx_ptp;
u64 eqx_tp;
u64 eqx_ssba;
u64 eqx_psba;
u64 eqx_cec;
u64 eqx_meql;
u64 eqx_xisbi;
u64 eqx_xisc;
u64 eqx_it;
};
static inline u64 epa_load(struct h_epa epa, u32 offset)
{
return readq((void __iomem *)(epa.addr + offset));
}
static inline void epa_store(struct h_epa epa, u32 offset, u64 value)
{
writeq(value, (void __iomem *)(epa.addr + offset));
epa_load(epa, offset); /* synchronize explicitly to eHEA */
}
static inline void epa_store_acc(struct h_epa epa, u32 offset, u64 value)
{
writeq(value, (void __iomem *)(epa.addr + offset));
}
#define epa_store_eq(epa, offset, value)\
epa_store(epa, EQTEMM_OFFSET(offset), value)
#define epa_load_eq(epa, offset)\
epa_load(epa, EQTEMM_OFFSET(offset))
#define epa_store_cq(epa, offset, value)\
epa_store(epa, CQTEMM_OFFSET(offset), value)
#define epa_load_cq(epa, offset)\
epa_load(epa, CQTEMM_OFFSET(offset))
#define epa_store_qp(epa, offset, value)\
epa_store(epa, QPTEMM_OFFSET(offset), value)
#define epa_load_qp(epa, offset)\
epa_load(epa, QPTEMM_OFFSET(offset))
#define epa_store_qped(epa, offset, value)\
epa_store(epa, QPEDMM_OFFSET(offset), value)
#define epa_load_qped(epa, offset)\
epa_load(epa, QPEDMM_OFFSET(offset))
#define epa_store_mrmw(epa, offset, value)\
epa_store(epa, MRMWMM_OFFSET(offset), value)
#define epa_load_mrmw(epa, offset)\
epa_load(epa, MRMWMM_OFFSET(offset))
#define epa_store_base(epa, offset, value)\
epa_store(epa, HCAGR_OFFSET(offset), value)
#define epa_load_base(epa, offset)\
epa_load(epa, HCAGR_OFFSET(offset))
static inline void ehea_update_sqa(struct ehea_qp *qp, u16 nr_wqes)
{
struct h_epa epa = qp->epas.kernel;
epa_store_acc(epa, QPTEMM_OFFSET(qpx_sqa),
EHEA_BMASK_SET(QPX_SQA_VALUE, nr_wqes));
}
static inline void ehea_update_rq3a(struct ehea_qp *qp, u16 nr_wqes)
{
struct h_epa epa = qp->epas.kernel;
epa_store_acc(epa, QPTEMM_OFFSET(qpx_rq3a),
EHEA_BMASK_SET(QPX_RQ1A_VALUE, nr_wqes));
}
static inline void ehea_update_rq2a(struct ehea_qp *qp, u16 nr_wqes)
{
struct h_epa epa = qp->epas.kernel;
epa_store_acc(epa, QPTEMM_OFFSET(qpx_rq2a),
EHEA_BMASK_SET(QPX_RQ2A_VALUE, nr_wqes));
}
static inline void ehea_update_rq1a(struct ehea_qp *qp, u16 nr_wqes)
{
struct h_epa epa = qp->epas.kernel;
epa_store_acc(epa, QPTEMM_OFFSET(qpx_rq1a),
EHEA_BMASK_SET(QPX_RQ3A_VALUE, nr_wqes));
}
static inline void ehea_update_feca(struct ehea_cq *cq, u32 nr_cqes)
{
struct h_epa epa = cq->epas.kernel;
epa_store_acc(epa, CQTEMM_OFFSET(cqx_feca),
EHEA_BMASK_SET(CQX_FECADDER, nr_cqes));
}
static inline void ehea_reset_cq_n1(struct ehea_cq *cq)
{
struct h_epa epa = cq->epas.kernel;
epa_store_cq(epa, cqx_n1,
EHEA_BMASK_SET(CQX_N1_GENERATE_COMP_EVENT, 1));
}
static inline void ehea_reset_cq_ep(struct ehea_cq *my_cq)
{
struct h_epa epa = my_cq->epas.kernel;
epa_store_acc(epa, CQTEMM_OFFSET(cqx_ep),
EHEA_BMASK_SET(CQX_EP_EVENT_PENDING, 0));
}
#endif /* __EHEA_HW_H__ */
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment