Commit 4df3c4e1 authored by Vincent Pelletier's avatar Vincent Pelletier

Factorise acceptIdentification checks.

parent 915bf256
...@@ -28,17 +28,13 @@ class PrimaryBootstrapHandler(AnswerBaseHandler): ...@@ -28,17 +28,13 @@ class PrimaryBootstrapHandler(AnswerBaseHandler):
app = self.app app = self.app
app.trying_master_node = None app.trying_master_node = None
def acceptIdentification(self, conn, node_type, uuid, num_partitions, def _acceptIdentification(self, node, uuid, num_partitions,
num_replicas, your_uuid, primary_uuid, known_master_list): num_replicas, your_uuid, primary_uuid, known_master_list):
app = self.app app = self.app
# this must be a master node
if node_type != NodeTypes.MASTER:
conn.close()
return
# Register new master nodes. # Register new master nodes.
found = False found = False
conn_address = conn.getAddress() conn_address = node.getAddress()
for node_address, node_uuid in known_master_list: for node_address, node_uuid in known_master_list:
if node_address == conn_address: if node_address == conn_address:
assert uuid == node_uuid, (dump(uuid), dump(node_uuid)) assert uuid == node_uuid, (dump(uuid), dump(node_uuid))
...@@ -48,8 +44,9 @@ class PrimaryBootstrapHandler(AnswerBaseHandler): ...@@ -48,8 +44,9 @@ class PrimaryBootstrapHandler(AnswerBaseHandler):
n = app.nm.createMaster(address=node_address) n = app.nm.createMaster(address=node_address)
if node_uuid is not None and n.getUUID() != node_uuid: if node_uuid is not None and n.getUUID() != node_uuid:
n.setUUID(node_uuid) n.setUUID(node_uuid)
assert found, (conn, dump(uuid), known_master_list) assert found, (node, dump(uuid), known_master_list)
conn = node.getConnection()
if primary_uuid is not None: if primary_uuid is not None:
primary_node = app.nm.getByUUID(primary_uuid) primary_node = app.nm.getByUUID(primary_uuid)
if primary_node is None: if primary_node is None:
......
...@@ -47,20 +47,12 @@ class StorageBootstrapHandler(AnswerBaseHandler): ...@@ -47,20 +47,12 @@ class StorageBootstrapHandler(AnswerBaseHandler):
def notReady(self, conn, message): def notReady(self, conn, message):
raise NodeNotReady(message) raise NodeNotReady(message)
def acceptIdentification(self, conn, node_type, def _acceptIdentification(self, node,
uuid, num_partitions, num_replicas, your_uuid, primary_uuid, uuid, num_partitions, num_replicas, your_uuid, primary_uuid,
master_list): master_list):
assert primary_uuid == self.app.primary_master_node.getUUID(), ( assert primary_uuid == self.app.primary_master_node.getUUID(), (
dump(primary_uuid), dump(self.app.primary_master_node.getUUID())) dump(primary_uuid), dump(self.app.primary_master_node.getUUID()))
# this must be a storage node
if node_type != NodeTypes.STORAGE:
conn.close()
return
node = self.app.nm.getByAddress(conn.getAddress())
assert node is not None, conn.getAddress()
node.setUUID(uuid) node.setUUID(uuid)
assert node.getConnection() is conn, (node.getConnection(), conn)
class StorageAnswersHandler(AnswerBaseHandler): class StorageAnswersHandler(AnswerBaseHandler):
""" Handle all messages related to ZODB operations """ """ Handle all messages related to ZODB operations """
......
...@@ -89,16 +89,16 @@ class BootstrapManager(EventHandler): ...@@ -89,16 +89,16 @@ class BootstrapManager(EventHandler):
""" """
conn.close() conn.close()
def acceptIdentification(self, conn, node_type, uuid, num_partitions, def _acceptIdentification(self, node, uuid, num_partitions,
num_replicas, your_uuid, primary_uuid, known_master_list): num_replicas, your_uuid, primary_uuid, known_master_list):
nm = self.app.nm nm = self.app.nm
# Register new master nodes. # Register new master nodes.
for address, uuid in known_master_list: for address, uuid in known_master_list:
node = nm.getByAddress(address) master_node = nm.getByAddress(address)
if node is None: if master_node is None:
node = nm.createMaster(address=address) master_node = nm.createMaster(address=address)
node.setUUID(uuid) master_node.setUUID(uuid)
self.primary = nm.getByUUID(primary_uuid) self.primary = nm.getByUUID(primary_uuid)
if self.primary is None or self.current is not self.primary: if self.primary is None or self.current is not self.primary:
...@@ -106,7 +106,7 @@ class BootstrapManager(EventHandler): ...@@ -106,7 +106,7 @@ class BootstrapManager(EventHandler):
# - something goes wrong (unknown UUID) # - something goes wrong (unknown UUID)
# - this master doesn't know who's the primary # - this master doesn't know who's the primary
# - got the primary's uuid, so cut here # - got the primary's uuid, so cut here
conn.close() node.getConnection().close()
return return
neo.lib.logging.info('connected to a primary master node') neo.lib.logging.info('connected to a primary master node')
......
...@@ -79,15 +79,9 @@ class ClientElectionHandler(BaseElectionHandler): ...@@ -79,15 +79,9 @@ class ClientElectionHandler(BaseElectionHandler):
self.app.unconnected_master_node_set.add(addr) self.app.unconnected_master_node_set.add(addr)
self.app.negotiating_master_node_set.discard(addr) self.app.negotiating_master_node_set.discard(addr)
def acceptIdentification(self, conn, node_type, peer_uuid, num_partitions, def _acceptIdentification(self, node, peer_uuid, num_partitions,
num_replicas, your_uuid, primary_uuid, known_master_list): num_replicas, your_uuid, primary_uuid, known_master_list):
app = self.app app = self.app
if node_type != NodeTypes.MASTER:
# The peer is not a master node!
neo.lib.logging.error('%r is not a master node', conn)
app.nm.remove(app.nm.getByAddress(conn.getAddress()))
conn.close()
return
if your_uuid != app.uuid: if your_uuid != app.uuid:
# uuid conflict happened, accept the new one and restart election # uuid conflict happened, accept the new one and restart election
...@@ -96,7 +90,7 @@ class ClientElectionHandler(BaseElectionHandler): ...@@ -96,7 +90,7 @@ class ClientElectionHandler(BaseElectionHandler):
dump(your_uuid)) dump(your_uuid))
raise ElectionFailure, 'new uuid supplied' raise ElectionFailure, 'new uuid supplied'
conn.setUUID(peer_uuid) node.setUUID(peer_uuid)
# Register new master nodes. # Register new master nodes.
for address, uuid in known_master_list: for address, uuid in known_master_list:
...@@ -135,7 +129,7 @@ class ClientElectionHandler(BaseElectionHandler): ...@@ -135,7 +129,7 @@ class ClientElectionHandler(BaseElectionHandler):
app.negotiating_master_node_set.clear() app.negotiating_master_node_set.clear()
return return
elect(app, peer_uuid, conn.getAddress()) elect(app, peer_uuid, node.getAddress())
class ServerElectionHandler(BaseElectionHandler, MasterHandler): class ServerElectionHandler(BaseElectionHandler, MasterHandler):
......
...@@ -84,18 +84,15 @@ class PrimaryHandler(EventHandler): ...@@ -84,18 +84,15 @@ class PrimaryHandler(EventHandler):
if n.getUUID() is None: if n.getUUID() is None:
n.setUUID(uuid) n.setUUID(uuid)
def acceptIdentification(self, conn, node_type, uuid, num_partitions, def _acceptIdentification(self, node, uuid, num_partitions,
num_replicas, your_uuid, primary_uuid, known_master_list): num_replicas, your_uuid, primary_uuid, known_master_list):
app = self.app app = self.app
if primary_uuid != app.primary_master_node.getUUID(): if primary_uuid != app.primary_master_node.getUUID():
raise PrimaryFailure('unexpected primary uuid') raise PrimaryFailure('unexpected primary uuid')
node = app.nm.getByAddress(conn.getAddress())
assert node_type == NodeTypes.MASTER
if your_uuid != app.uuid: if your_uuid != app.uuid:
# uuid conflict happened, accept the new one # uuid conflict happened, accept the new one
app.uuid = your_uuid app.uuid = your_uuid
conn.setUUID(uuid)
node.setUUID(uuid) node.setUUID(uuid)
...@@ -333,13 +333,15 @@ class NeoUnitTestBase(NeoTestBase): ...@@ -333,13 +333,15 @@ class NeoUnitTestBase(NeoTestBase):
""" ensure no UUID was set on the connection """ """ ensure no UUID was set on the connection """
self.assertEqual(len(conn.mockGetNamedCalls('setUUID')), 0) self.assertEqual(len(conn.mockGetNamedCalls('setUUID')), 0)
def checkUUIDSet(self, conn, uuid=None): def checkUUIDSet(self, conn, uuid=None, check_intermediate=True):
""" ensure no UUID was set on the connection """ """ ensure UUID was set on the connection """
calls = conn.mockGetNamedCalls('setUUID') calls = conn.mockGetNamedCalls('setUUID')
self.assertEqual(len(calls), 1) found_uuid = calls.pop().getParam(0)
call = calls.pop() if check_intermediate:
for call in calls:
self.assertEqual(found_uuid, call.getParam(0))
if uuid is not None: if uuid is not None:
self.assertEqual(call.getParam(0), uuid) self.assertEqual(found_uuid, uuid)
# in check(Ask|Answer|Notify)Packet we return the packet so it can be used # in check(Ask|Answer|Notify)Packet we return the packet so it can be used
# in tests if more accurates checks are required # in tests if more accurates checks are required
......
...@@ -32,6 +32,16 @@ class MasterHandlerTests(NeoUnitTestBase): ...@@ -32,6 +32,16 @@ class MasterHandlerTests(NeoUnitTestBase):
self.app = Mock({'getDB': self.db}) self.app = Mock({'getDB': self.db})
self.app.nm = NodeManager() self.app.nm = NodeManager()
self.app.dispatcher = Mock() self.app.dispatcher = Mock()
self._next_port = 3000
def getKnownMaster(self):
node = self.app.nm.createMaster(address=(
self.local_ip, self._next_port),
)
self._next_port += 1
conn = self.getFakeConnection(address=node.getAddress())
node.setConnection(conn)
return node, conn
class MasterBootstrapHandlerTests(MasterHandlerTests): class MasterBootstrapHandlerTests(MasterHandlerTests):
...@@ -51,18 +61,15 @@ class MasterBootstrapHandlerTests(MasterHandlerTests): ...@@ -51,18 +61,15 @@ class MasterBootstrapHandlerTests(MasterHandlerTests):
def test_acceptIdentification1(self): def test_acceptIdentification1(self):
""" Non-master node """ """ Non-master node """
conn = self.getFakeConnection() node, conn = self.getKnownMaster()
uuid = self.getNewUUID()
self.handler.acceptIdentification(conn, NodeTypes.CLIENT, self.handler.acceptIdentification(conn, NodeTypes.CLIENT,
uuid, 100, 0, None, None, []) node.getUUID(), 100, 0, None, None, [])
self.checkClosed(conn) self.checkClosed(conn)
def test_acceptIdentification2(self): def test_acceptIdentification2(self):
""" No UUID supplied """ """ No UUID supplied """
conn = self.getFakeConnection() node, conn = self.getKnownMaster()
uuid = self.getNewUUID() uuid = self.getNewUUID()
node = Mock()
self.app.nm = Mock({'getByAddress': node, 'getByUUID': node})
self.checkProtocolErrorRaised(self.handler.acceptIdentification, self.checkProtocolErrorRaised(self.handler.acceptIdentification,
conn, NodeTypes.MASTER, uuid, 100, 0, None, conn, NodeTypes.MASTER, uuid, 100, 0, None,
uuid, [(conn.getAddress(), uuid)], uuid, [(conn.getAddress(), uuid)],
...@@ -70,17 +77,13 @@ class MasterBootstrapHandlerTests(MasterHandlerTests): ...@@ -70,17 +77,13 @@ class MasterBootstrapHandlerTests(MasterHandlerTests):
def test_acceptIdentification3(self): def test_acceptIdentification3(self):
""" identification accepted """ """ identification accepted """
node = Mock() node, conn = self.getKnownMaster()
conn = self.getFakeConnection()
uuid = self.getNewUUID() uuid = self.getNewUUID()
your_uuid = self.getNewUUID() your_uuid = self.getNewUUID()
partitions = 100
replicas = 2
self.app.nm = Mock({'getByAddress': node, 'getByUUID': node})
self.handler.acceptIdentification(conn, NodeTypes.MASTER, uuid, self.handler.acceptIdentification(conn, NodeTypes.MASTER, uuid,
partitions, replicas, your_uuid, uuid, [(conn.getAddress(), uuid)]) 100, 2, your_uuid, uuid, [(conn.getAddress(), uuid)])
self.assertEqual(self.app.uuid, your_uuid) self.assertEqual(self.app.uuid, your_uuid)
self.checkUUIDSet(node, uuid) self.assertEqual(node.getUUID(), uuid)
self.assertTrue(isinstance(self.app.pt, PartitionTable)) self.assertTrue(isinstance(self.app.pt, PartitionTable))
def _getMasterList(self, uuid_list): def _getMasterList(self, uuid_list):
......
...@@ -24,6 +24,7 @@ from neo.client.exception import NEOStorageError, NEOStorageNotFoundError ...@@ -24,6 +24,7 @@ from neo.client.exception import NEOStorageError, NEOStorageNotFoundError
from neo.client.exception import NEOStorageDoesNotExistError from neo.client.exception import NEOStorageDoesNotExistError
from ZODB.POSException import ConflictError from ZODB.POSException import ConflictError
from neo.lib.exception import NodeNotReady from neo.lib.exception import NodeNotReady
from neo.lib.node import NodeManager
from ZODB.TimeStamp import TimeStamp from ZODB.TimeStamp import TimeStamp
MARKER = [] MARKER = []
...@@ -33,7 +34,24 @@ class StorageBootstrapHandlerTests(NeoUnitTestBase): ...@@ -33,7 +34,24 @@ class StorageBootstrapHandlerTests(NeoUnitTestBase):
def setUp(self): def setUp(self):
super(NeoUnitTestBase, self).setUp() super(NeoUnitTestBase, self).setUp()
self.app = Mock() self.app = Mock()
self.app.nm = NodeManager()
self.handler = StorageBootstrapHandler(self.app) self.handler = StorageBootstrapHandler(self.app)
self.app.primary_master_node = node = Mock({
'getConnection': self.getFakeConnection(),
'getUUID': self.getNewUUID(),
})
self._next_port = 3000
def getKnownStorage(self):
node = self.app.nm.createStorage(
uuid=self.getNewUUID(),
address=(self.local_ip, self._next_port),
)
self._next_port += 1
conn = self.getFakeConnection(address=node.getAddress(),
uuid=node.getUUID())
node.setConnection(conn)
return node, conn
def test_notReady(self): def test_notReady(self):
conn = self.getFakeConnection() conn = self.getFakeConnection()
...@@ -41,26 +59,18 @@ class StorageBootstrapHandlerTests(NeoUnitTestBase): ...@@ -41,26 +59,18 @@ class StorageBootstrapHandlerTests(NeoUnitTestBase):
def test_acceptIdentification1(self): def test_acceptIdentification1(self):
""" Not a storage node """ """ Not a storage node """
uuid = self.getNewUUID() node, conn = self.getKnownStorage()
node_uuid = self.getNewUUID() self.handler.acceptIdentification(conn, NodeTypes.CLIENT,
conn = self.getFakeConnection() node.getUUID(),
self.app.primary_master_node = node = Mock({'getUUID': node_uuid}) 10, 0, None, self.app.primary_master_node.getUUID(), [])
self.app.nm = Mock({'getByAddress': node})
self.handler.acceptIdentification(conn, NodeTypes.CLIENT, uuid,
10, 0, None, node_uuid, [])
self.checkClosed(conn) self.checkClosed(conn)
def test_acceptIdentification2(self): def test_acceptIdentification2(self):
uuid = self.getNewUUID() node, conn = self.getKnownStorage()
node_uuid = self.getNewUUID() self.handler.acceptIdentification(conn, NodeTypes.STORAGE,
conn = self.getFakeConnection() node.getUUID(),
self.app.primary_master_node = node = Mock({'getConnection': conn, 10, 0, None, self.app.primary_master_node.getUUID(), [])
'getUUID': node_uuid}) self.checkNotClosed(conn)
self.app.nm = Mock({'getByAddress': node})
self.handler.acceptIdentification(conn, NodeTypes.STORAGE, uuid,
10, 0, None, node_uuid, [])
self.checkUUIDSet(node, uuid)
class StorageAnswerHandlerTests(NeoUnitTestBase): class StorageAnswerHandlerTests(NeoUnitTestBase):
......
...@@ -31,7 +31,26 @@ def _addPacket(self, packet): ...@@ -31,7 +31,26 @@ def _addPacket(self, packet):
if self.connector is not None: if self.connector is not None:
self.connector._addPacket(packet) self.connector._addPacket(packet)
class MasterClientElectionTests(NeoUnitTestBase): class MasterClientElectionTestBase(NeoUnitTestBase):
def setUp(self):
super(MasterClientElectionTestBase, self).setUp()
self._master_port = 3001
def identifyToMasterNode(self, uuid=True):
if uuid is True:
uuid = self.getNewUUID()
node = self.app.nm.createMaster(uuid=uuid)
node.setAddress((self.local_ip, self._master_port))
self._master_port += 1
conn = self.getFakeConnection(
uuid=node.getUUID(),
address=node.getAddress(),
)
node.setConnection(conn)
return (node, conn)
class MasterClientElectionTests(MasterClientElectionTestBase):
def setUp(self): def setUp(self):
NeoUnitTestBase.setUp(self) NeoUnitTestBase.setUp(self)
...@@ -49,19 +68,13 @@ class MasterClientElectionTests(NeoUnitTestBase): ...@@ -49,19 +68,13 @@ class MasterClientElectionTests(NeoUnitTestBase):
# apply monkey patches # apply monkey patches
self._addPacket = ClientConnection._addPacket self._addPacket = ClientConnection._addPacket
ClientConnection._addPacket = _addPacket ClientConnection._addPacket = _addPacket
super(MasterClientElectionTests, self).setUp()
def tearDown(self): def tearDown(self):
# restore patched methods # restore patched methods
ClientConnection._addPacket = self._addPacket ClientConnection._addPacket = self._addPacket
NeoUnitTestBase.tearDown(self) NeoUnitTestBase.tearDown(self)
def identifyToMasterNode(self):
node = self.app.nm.getMasterList()[0]
node.setUUID(self.getNewUUID())
conn = self.getFakeConnection(uuid=node.getUUID(),
address=node.getAddress())
return (node, conn)
def _checkUnconnected(self, node): def _checkUnconnected(self, node):
addr = node.getAddress() addr = node.getAddress()
self.assertFalse(addr in self.app.negotiating_master_node_set) self.assertFalse(addr in self.app.negotiating_master_node_set)
...@@ -107,9 +120,8 @@ class MasterClientElectionTests(NeoUnitTestBase): ...@@ -107,9 +120,8 @@ class MasterClientElectionTests(NeoUnitTestBase):
self.checkClosed(conn) self.checkClosed(conn)
def test_acceptIdentificationDoesNotKnowPrimary(self): def test_acceptIdentificationDoesNotKnowPrimary(self):
master1_uuid = self.getNewUUID() master1, master1_conn = self.identifyToMasterNode()
master1_address = ('127.0.0.1', 2001) master1_uuid = master1.getUUID()
master1_conn = self.getFakeConnection(address=master1_address)
self.election.acceptIdentification( self.election.acceptIdentification(
master1_conn, master1_conn,
NodeTypes.MASTER, NodeTypes.MASTER,
...@@ -118,14 +130,13 @@ class MasterClientElectionTests(NeoUnitTestBase): ...@@ -118,14 +130,13 @@ class MasterClientElectionTests(NeoUnitTestBase):
0, 0,
self.app.uuid, self.app.uuid,
None, None,
[(master1_address, master1_uuid)], [(master1.getAddress(), master1_uuid)],
) )
self.assertEqual(self.app.primary_master_node, None) self.assertEqual(self.app.primary_master_node, None)
def test_acceptIdentificationKnowsPrimary(self): def test_acceptIdentificationKnowsPrimary(self):
master1_uuid = self.getNewUUID() master1, master1_conn = self.identifyToMasterNode()
master1_address = ('127.0.0.1', 2001) master1_uuid = master1.getUUID()
master1_conn = self.getFakeConnection(address=master1_address)
self.election.acceptIdentification( self.election.acceptIdentification(
master1_conn, master1_conn,
NodeTypes.MASTER, NodeTypes.MASTER,
...@@ -134,19 +145,20 @@ class MasterClientElectionTests(NeoUnitTestBase): ...@@ -134,19 +145,20 @@ class MasterClientElectionTests(NeoUnitTestBase):
0, 0,
self.app.uuid, self.app.uuid,
master1_uuid, master1_uuid,
[(master1_address, master1_uuid)], [(master1.getAddress(), master1_uuid)],
) )
self.assertNotEqual(self.app.primary_master_node, None) self.assertNotEqual(self.app.primary_master_node, None)
def test_acceptIdentificationMultiplePrimaries(self): def test_acceptIdentificationMultiplePrimaries(self):
master1_uuid = self.getNewUUID() master1, master1_conn = self.identifyToMasterNode()
master2_uuid = self.getNewUUID() master2, master2_conn = self.identifyToMasterNode()
master3_uuid = self.getNewUUID() master3, _ = self.identifyToMasterNode()
master1_address = ('127.0.0.1', 2001) master1_uuid = master1.getUUID()
master2_address = ('127.0.0.1', 2002) master2_uuid = master2.getUUID()
master3_address = ('127.0.0.1', 2003) master3_uuid = master3.getUUID()
master1_conn = self.getFakeConnection(address=master1_address) master1_address = master1.getAddress()
master2_conn = self.getFakeConnection(address=master2_address) master2_address = master2.getAddress()
master3_address = master3.getAddress()
self.election.acceptIdentification( self.election.acceptIdentification(
master1_conn, master1_conn,
NodeTypes.MASTER, NodeTypes.MASTER,
...@@ -197,7 +209,7 @@ class MasterClientElectionTests(NeoUnitTestBase): ...@@ -197,7 +209,7 @@ class MasterClientElectionTests(NeoUnitTestBase):
return [(x.getAddress(), x.getUUID()) for x in master_list] return [(x.getAddress(), x.getUUID()) for x in master_list]
class MasterServerElectionTests(NeoUnitTestBase): class MasterServerElectionTests(MasterClientElectionTestBase):
def setUp(self): def setUp(self):
NeoUnitTestBase.setUp(self) NeoUnitTestBase.setUp(self)
...@@ -219,26 +231,13 @@ class MasterServerElectionTests(NeoUnitTestBase): ...@@ -219,26 +231,13 @@ class MasterServerElectionTests(NeoUnitTestBase):
# apply monkey patches # apply monkey patches
self._addPacket = ClientConnection._addPacket self._addPacket = ClientConnection._addPacket
ClientConnection._addPacket = _addPacket ClientConnection._addPacket = _addPacket
super(MasterServerElectionTests, self).setUp()
def tearDown(self): def tearDown(self):
NeoUnitTestBase.tearDown(self) NeoUnitTestBase.tearDown(self)
# restore environnement # restore environnement
ClientConnection._addPacket = self._addPacket ClientConnection._addPacket = self._addPacket
def identifyToMasterNode(self, uuid=True):
node = self.app.nm.getMasterList()[0]
if uuid is True:
uuid = self.getNewUUID()
node.setUUID(uuid)
conn = self.getFakeConnection(
uuid=node.getUUID(),
address=node.getAddress(),
)
return (node, conn)
# Tests
def test_requestIdentification1(self): def test_requestIdentification1(self):
""" A non-master node request identification """ """ A non-master node request identification """
node, conn = self.identifyToMasterNode() node, conn = self.identifyToMasterNode()
...@@ -275,7 +274,7 @@ class MasterServerElectionTests(NeoUnitTestBase): ...@@ -275,7 +274,7 @@ class MasterServerElectionTests(NeoUnitTestBase):
args = (self.app.uuid, node.getAddress(), self.app.name) args = (self.app.uuid, node.getAddress(), self.app.name)
self.election.requestIdentification(conn, self.election.requestIdentification(conn,
NodeTypes.MASTER, *args) NodeTypes.MASTER, *args)
self.checkUUIDSet(conn) self.checkUUIDSet(conn, check_intermediate=False)
args = self.checkAcceptIdentification(conn, decode=True) args = self.checkAcceptIdentification(conn, decode=True)
(node_type, uuid, partitions, replicas, new_uuid, primary_uuid, (node_type, uuid, partitions, replicas, new_uuid, primary_uuid,
master_list) = args master_list) = args
......
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