Commit 56120543 authored by Cedric CAFFY's avatar Cedric CAFFY
Browse files

Tape session started logs finished

parent 7738e880
......@@ -72,14 +72,29 @@ std::string cta::ArchiveMount::getPoolName() const {
// getVo
//------------------------------------------------------------------------------
std::string cta::ArchiveMount::getVo() const {
return "Alice";//m_dbMount->mountInfo.vo;
return m_dbMount->mountInfo.vo;
}
//------------------------------------------------------------------------------
// getVo
// getMediaType
//------------------------------------------------------------------------------
std::string cta::ArchiveMount::getMediaType() const{
return m_dbMount->mountInfo.mediaType;
}
//------------------------------------------------------------------------------
// getVendor
//------------------------------------------------------------------------------
std::string cta::ArchiveMount::getVendor() const{
return m_dbMount->mountInfo.vendor;
}
//------------------------------------------------------------------------------
// getCapacityInBytes
//------------------------------------------------------------------------------
std::string cta::ArchiveMount::getDensity() const{
return "8000GC";//m_dbMount->mountInfo.density;
uint64_t cta::ArchiveMount::getCapacityInBytes() const
{
return m_dbMount->mountInfo.capacityInBytes;
}
//------------------------------------------------------------------------------
......
......@@ -148,10 +148,22 @@ namespace cta {
std::string getVo() const;
/**
* Returns the density (e.g : 8000GC) of the tape to be mounted
* @return the density of the tape to be mounted
* Returns the media type of the tape
* @return de media type of the tape
*/
std::string getDensity() const;
std::string getMediaType() const;
/**
* Returns the vendor of the tape
* @return the vendor of the tape
*/
std::string getVendor() const;
/**
* Returns the capacity in bytes of the tape
* @return the capacity in bytes of the tape
*/
uint64_t getCapacityInBytes() const;
/**
* Returns the mount transaction id.
......
......@@ -428,14 +428,32 @@ void OStoreDB::trimEmptyQueues(log::LogContext& lc) {
//------------------------------------------------------------------------------
// OStoreDB::TapeMountDecisionInfoNoLock::createArchiveMount()
//------------------------------------------------------------------------------
std::unique_ptr<SchedulerDatabase::ArchiveMount> OStoreDB::TapeMountDecisionInfoNoLock::createArchiveMount(const catalogue::TapeForWriting& tape, const std::string driveName, const std::string& logicalLibrary, const std::string& hostName, time_t startTime) {
std::unique_ptr<SchedulerDatabase::ArchiveMount> OStoreDB::TapeMountDecisionInfoNoLock::createArchiveMount(
const catalogue::TapeForWriting& tape,
const std::string driveName,
const std::string& logicalLibrary,
const std::string& hostName,
const std::string& vo,
const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) {
throw cta::exception::Exception("In OStoreDB::TapeMountDecisionInfoNoLock::createArchiveMount(): This function should not be called");
}
//------------------------------------------------------------------------------
// OStoreDB::TapeMountDecisionInfoNoLock::createRetrieveMount()
//------------------------------------------------------------------------------
std::unique_ptr<SchedulerDatabase::RetrieveMount> OStoreDB::TapeMountDecisionInfoNoLock::createRetrieveMount(const std::string& vid, const std::string& tapePool, const std::string driveName, const std::string& logicalLibrary, const std::string& hostName, time_t startTime) {
std::unique_ptr<SchedulerDatabase::RetrieveMount> OStoreDB::TapeMountDecisionInfoNoLock::createRetrieveMount(const std::string& vid,
const std::string& tapePool,
const std::string driveName,
const std::string& logicalLibrary,
const std::string& hostName,
const std::string& vo,
const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) {
throw cta::exception::Exception("In OStoreDB::TapeMountDecisionInfoNoLock::createRetrieveMount(): This function should not be called");
}
......@@ -1501,7 +1519,8 @@ void OStoreDB::setDriveShutdown(common::dataStructures::DriveState & driveState,
std::unique_ptr<SchedulerDatabase::ArchiveMount>
OStoreDB::TapeMountDecisionInfo::createArchiveMount(
const catalogue::TapeForWriting & tape, const std::string driveName,
const std::string& logicalLibrary, const std::string& hostName, time_t startTime) {
const std::string& logicalLibrary, const std::string& hostName, const std::string& vo, const std::string& mediaType,
const std::string& vendor,uint64_t capacityInBytes, time_t startTime) {
// In order to create the mount, we have to:
// Check we actually hold the scheduling lock
// Set the drive status to up, and indicate which tape we use.
......@@ -1520,7 +1539,11 @@ std::unique_ptr<SchedulerDatabase::ArchiveMount>
// Fill up the mount info
am.mountInfo.drive = driveName;
am.mountInfo.host = hostName;
am.mountInfo.vo = vo;
am.mountInfo.mediaType = mediaType;
am.mountInfo.vendor = vendor;
am.mountInfo.mountId = m_schedulerGlobalLock->getIncreaseCommitMountId();
am.mountInfo.capacityInBytes = capacityInBytes;
m_schedulerGlobalLock->commit();
am.mountInfo.tapePool = tape.tapePool;
am.mountInfo.logicalLibrary = logicalLibrary;
......@@ -1565,7 +1588,8 @@ OStoreDB::TapeMountDecisionInfo::TapeMountDecisionInfo(OStoreDB & oStroeDb): m_l
std::unique_ptr<SchedulerDatabase::RetrieveMount>
OStoreDB::TapeMountDecisionInfo::createRetrieveMount(
const std::string& vid, const std::string & tapePool, const std::string driveName,
const std::string& logicalLibrary, const std::string& hostName, time_t startTime) {
const std::string& logicalLibrary, const std::string& hostName,const std::string& vo, const std::string& mediaType,
const std::string& vendor,const uint64_t capacityInBytes, time_t startTime) {
// In order to create the mount, we have to:
// Check we actually hold the scheduling lock
// Check the tape exists, add it to ownership and set its activity status to
......@@ -1593,6 +1617,10 @@ std::unique_ptr<SchedulerDatabase::RetrieveMount>
m_schedulerGlobalLock->commit();
rm.mountInfo.tapePool = tapePool;
rm.mountInfo.logicalLibrary = logicalLibrary;
rm.mountInfo.vo = vo;
rm.mountInfo.mediaType = mediaType;
rm.mountInfo.vendor = vendor;
rm.mountInfo.capacityInBytes = capacityInBytes;
// Update the status of the drive in the registry
{
// Get hold of the drive registry
......
......@@ -94,11 +94,17 @@ public:
std::unique_ptr<SchedulerDatabase::ArchiveMount> createArchiveMount(
const catalogue::TapeForWriting & tape,
const std::string driveName, const std::string& logicalLibrary,
const std::string & hostName, time_t startTime) override;
const std::string & hostName,
const std::string& vo, const std::string& mediaType,
const std::string& vendor,uint64_t capacityInBytes,
time_t startTime) override;
std::unique_ptr<SchedulerDatabase::RetrieveMount> createRetrieveMount(
const std::string & vid, const std::string & tapePool,
const std::string driveName,
const std::string& logicalLibrary, const std::string& hostName,
const std::string& vo, const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) override;
virtual ~TapeMountDecisionInfo();
private:
......@@ -115,11 +121,16 @@ public:
std::unique_ptr<SchedulerDatabase::ArchiveMount> createArchiveMount(
const catalogue::TapeForWriting & tape,
const std::string driveName, const std::string& logicalLibrary,
const std::string & hostName, time_t startTime) override;
const std::string & hostName, const std::string& vo, const std::string& mediaType,
const std::string& vendor,uint64_t capacityInBytes,
time_t startTime) override;
std::unique_ptr<SchedulerDatabase::RetrieveMount> createRetrieveMount(
const std::string & vid, const std::string & tapePool,
const std::string driveName,
const std::string& logicalLibrary, const std::string& hostName,
const std::string& vo, const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) override;
virtual ~TapeMountDecisionInfoNoLock();
};
......
......@@ -166,7 +166,7 @@ TEST_P(OStoreDBTest, getBatchArchiveJob) {
tape.lastFSeq = 1;
tape.tapePool = "Tapepool1";
tape.vid = "tape";
auto mount = mountInfo->createArchiveMount(tape, "drive", "library", "host", ::time(nullptr));
auto mount = mountInfo->createArchiveMount(tape, "drive", "library", "host", "vo","mediaType","vendor",123456789,::time(nullptr));
auto giveAll = std::numeric_limits<uint64_t>::max();
auto jobs = mount->getNextJobBatch(giveAll, giveAll, lc);
ASSERT_EQ(8, jobs.size());
......
......@@ -85,17 +85,38 @@ std::string cta::RetrieveMount::getVo() const
std::stringstream sVo;
if(!m_dbMount.get())
throw exception::Exception("In cta::RetrieveMount::getVo(): got NULL dbMount");
sVo<<"Alice";//m_dbMount->mountInfo.vo;
sVo<<m_dbMount->mountInfo.vo;
return sVo.str();
}
std::string cta::RetrieveMount::getDensity() const
//------------------------------------------------------------------------------
// getMediaType()
//------------------------------------------------------------------------------
std::string cta::RetrieveMount::getMediaType() const
{
std::stringstream sMediaType;
if(!m_dbMount.get())
throw exception::Exception("In cta::RetrieveMount::getMediaType(): got NULL dbMount");
sMediaType<<m_dbMount->mountInfo.mediaType;
return sMediaType.str();
}
//------------------------------------------------------------------------------
// getVo()
//------------------------------------------------------------------------------
std::string cta::RetrieveMount::getVendor() const
{
std::stringstream sDensity;
std::stringstream sVendor;
if(!m_dbMount.get())
throw exception::Exception("In cta::RetrieveMount::getVendor(): got NULL dbMount");
sVendor<<m_dbMount->mountInfo.vendor;
return sVendor.str();
}
uint64_t cta::RetrieveMount::getCapacityInBytes() const {
if(!m_dbMount.get())
throw exception::Exception("In cta::RetrieveMount::getDensity(): got NULL dbMount");
sDensity<<"8000GC";//m_dbMount->mountInfo.density;
return sDensity.str();
throw exception::Exception("In cta::RetrieveMount::getVendor(): got NULL dbMount");
return m_dbMount->mountInfo.capacityInBytes;
}
//------------------------------------------------------------------------------
......
......@@ -97,10 +97,22 @@ namespace cta {
std::string getVo() const;
/**
* Returns the density (e.g : 8000GC) of the tape
* @return de density of the tape
* Returns the media type of the tape
* @return de media type of the tape
*/
std::string getDensity() const;
std::string getMediaType() const;
/**
* Returns the vendor of the tape
* @return the vendor of the tape
*/
std::string getVendor() const;
/**
* Returns the capacity in bytes of the tape
* @return the capacity in bytes of the tape
*/
uint64_t getCapacityInBytes() const;
/**
* Report a drive status change
......
......@@ -531,6 +531,10 @@ void Scheduler::sortAndGetTapesForMountInfo(std::unique_ptr<SchedulerDatabase::T
if (m.type==common::dataStructures::MountType::Retrieve) {
m.logicalLibrary=tapesInfo[m.vid].logicalLibraryName;
m.tapePool=tapesInfo[m.vid].tapePoolName;
m.vendor = tapesInfo[m.vid].vendor;
m.mediaType = tapesInfo[m.vid].mediaType;
m.vo = tapesInfo[m.vid].vo;
m.capacityInBytes = tapesInfo[m.vid].capacityInBytes;
}
}
}
......@@ -838,7 +842,11 @@ std::unique_ptr<TapeMount> Scheduler::getNextMount(const std::string &logicalLib
internalRet->m_dbMount.reset(mountInfo->createArchiveMount(t,
driveName,
logicalLibraryName,
utils::getShortHostname(),
utils::getShortHostname(),
t.vo,
t.mediaType,
t.vendor,
t.capacityInBytes,
time(NULL)).release());
mountCreationTime += timer.secs(utils::Timer::resetCounter);
internalRet->m_sessionRunning = true;
......@@ -854,6 +862,9 @@ std::unique_ptr<TapeMount> Scheduler::getNextMount(const std::string &logicalLib
params.add("tapepool", m->tapePool)
.add("vid", t.vid)
.add("vo",t.vo)
.add("mediaType",t.mediaType)
.add("vendor",t.vendor)
.add("mountType", common::dataStructures::toString(m->type))
.add("existingMounts", existingMounts)
.add("bytesQueued", m->bytesQueued)
......@@ -896,6 +907,10 @@ std::unique_ptr<TapeMount> Scheduler::getNextMount(const std::string &logicalLib
driveName,
logicalLibraryName,
utils::getShortHostname(),
m->vo,
m->mediaType,
m->vendor,
m->capacityInBytes,
time(NULL))));
mountCreationTime += timer.secs(utils::Timer::resetCounter);
internalRet->m_sessionRunning = true;
......@@ -912,6 +927,9 @@ std::unique_ptr<TapeMount> Scheduler::getNextMount(const std::string &logicalLib
catalogueTime = getTapeInfoTime + getTapeForWriteTime;
params.add("tapepool", m->tapePool)
.add("vid", m->vid)
.add("vo",m->vo)
.add("mediaType",m->mediaType)
.add("vendor",m->vendor)
.add("mountType", common::dataStructures::toString(m->type))
.add("existingMounts", existingMounts)
.add("bytesQueued", m->bytesQueued)
......
......@@ -145,10 +145,12 @@ public:
std::string logicalLibrary;
std::string tapePool;
std::string vo;
std::string density;
std::string mediaType;
std::string vendor;
std::string drive;
std::string host;
uint64_t mountId;
uint64_t capacityInBytes;
} mountInfo;
virtual const MountInfo & getMountInfo() = 0;
virtual std::list<std::unique_ptr<ArchiveJob>> getNextJobBatch(uint64_t filesRequested,
......@@ -301,9 +303,11 @@ public:
std::string logicalLibrary;
std::string tapePool;
std::string vo;
std::string density;
std::string mediaType;
std::string vendor;
std::string drive;
std::string host;
uint64_t capacityInBytes;
uint64_t mountId;
} mountInfo;
virtual const MountInfo & getMountInfo() = 0;
......@@ -346,6 +350,11 @@ public:
std::string vid; /**< The tape VID (for a retieve) */
std::string tapePool; /**< The name of the tape pool for both archive
* and retrieve */
std::string vo; // Virtual organization of the tape
std::string mediaType; // Media type of the tape
std::string vendor; // Vendor of the tape
uint64_t capacityInBytes; // Capacity in bytes of the tape
uint64_t priority; /**< The priority for the mount, defined as the highest
* priority of all queued jobs */
uint64_t maxDrivesAllowed; /**< The maximum number of drives allowed for this
......@@ -417,7 +426,10 @@ public:
*/
virtual std::unique_ptr<ArchiveMount> createArchiveMount(
const catalogue::TapeForWriting & tape, const std::string driveName,
const std::string & logicalLibrary, const std::string & hostName,
const std::string & logicalLibrary, const std::string & hostName,
const std::string& vo, const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) = 0;
/**
* Create a new retrieve mount. This implicitly releases the global scheduling
......@@ -426,6 +438,9 @@ public:
virtual std::unique_ptr<RetrieveMount> createRetrieveMount(const std::string & vid,
const std::string & tapePool, const std::string driveName,
const std::string& logicalLibrary, const std::string& hostName,
const std::string& vo, const std::string& mediaType,
const std::string& vendor,
const uint64_t capacityInBytes,
time_t startTime) = 0;
/** Destructor: releases the global lock if not already done */
virtual ~TapeMountDecisionInfo() {};
......
......@@ -195,7 +195,7 @@ TEST_P(SchedulerDatabaseTest, createManyArchiveJobs) {
cta::catalogue::TapeForWriting tfw;
tfw.tapePool = "tapePool";
tfw.vid = "vid";
auto am = moutInfo->createArchiveMount(tfw, "drive", "library", "host", time(nullptr));
auto am = moutInfo->createArchiveMount(tfw, "drive", "library", "host", "vo","mediaType", "vendor",123456789,time(nullptr));
bool done = false;
size_t count = 0;
while (!done) {
......@@ -276,12 +276,12 @@ TEST_P(SchedulerDatabaseTest, createManyArchiveJobs) {
cta::catalogue::TapeForWriting tfw;
tfw.tapePool = "tapePool";
tfw.vid = "vid";
auto am = moutInfo->createArchiveMount(tfw, "drive", "library", "host", time(nullptr));
auto am = moutInfo->createArchiveMount(tfw, "drive", "library", "host","vo","mediaType", "vendor",123456789, time(nullptr));
auto done = false;
auto count = 0;
#else
moutInfo = db.getMountInfo(lc);
am = moutInfo->createArchiveMount(tfw, "drive", "library", "host", time(nullptr));
am = moutInfo->createArchiveMount(tfw, "drive", "library", "host", "vo","mediaType", "vendor",123456789,time(nullptr));
done = false;
count = 0;
#endif
......
......@@ -62,7 +62,15 @@ namespace cta {
virtual std::string getVo() const = 0;
virtual std::string getDensity() const = 0;
virtual std::string getMediaType() const = 0;
virtual std::string getVendor() const = 0;
/**
* Returns the capacity in bytes of the tape
* @return the capacity in bytes of the tape
*/
uint64_t getCapacityInBytes() const;
/**
* Indicates that the mount was aborted.
......
......@@ -45,9 +45,14 @@ class TapeMountDummy: public TapeMount {
throw exception::Exception("In DummyTapeMount::getVo() : not implemented");
}
std::string getDensity() const override {
throw exception::Exception("In DummyTapeMount::getDensity() : not implemented");
std::string getMediaType() const override {
throw exception::Exception("In DummyTapeMount::getMediaType() : not implemented");
}
std::string getVendor() const override{
throw exception::Exception("In DummyTapeMount::getVendor() : not implemented");
}
void setDriveStatus(cta::common::dataStructures::DriveStatus status) override {}
void setTapeSessionStats(const castor::tape::tapeserver::daemon::TapeSessionStats &stats) override {};
};
......
......@@ -106,7 +106,7 @@ castor::tape::tapeserver::daemon::Session::EndOfSessionAction
cta::log::LogContext lc(m_log);
// Create a sticky thread name, which will be overridden by the other threads
lc.pushOrReplace(cta::log::Param("thread", "MainThread"));
lc.pushOrReplace(cta::log::Param("unitName", m_driveConfig.unitName));
lc.pushOrReplace(cta::log::Param("tapeDrive", m_driveConfig.unitName));
setProcessCapabilities("cap_sys_rawio+ep");
......@@ -182,7 +182,7 @@ schedule:
// 2c) ... and log.
// Make the DGN and TPVID parameter permanent.
cta::log::ScopedParamContainer params(lc);
params.add("vid", m_volInfo.vid)
params.add("tapeVid", m_volInfo.vid)
.add("mountId", tapeMount->getMountTransactionId());
{
cta::log::ScopedParamContainer localParams(lc);
......
......@@ -179,10 +179,14 @@ castor::tape::tapeserver::daemon::TapeReadSingleThread::openReadSession() {
new castor::tape::tapeFile::ReadSession(m_drive,m_volInfo, m_useLbp));
cta::log::ScopedParamContainer params(m_logContext);
params.add("vo",m_retrieveMount.getVo());
params.add("capacity",m_retrieveMount.getDensity());
params.add("tapePoolName",m_retrieveMount.getPoolName());
params.add("dgn",m_drive.config.logicalLibrary);
m_logContext.log(cta::log::DEBUG, "Created tapeFile::ReadSession with success");
params.add("mediaType",m_retrieveMount.getMediaType());
params.add("tapePool",m_retrieveMount.getPoolName());
params.add("logicalLibrary",m_drive.config.logicalLibrary);
params.add("mountType",mountTypeToString(m_volInfo.mountType));
params.add("vendor",m_retrieveMount.getVendor());
params.add("capacityInBytes",m_retrieveMount.getCapacityInBytes());
m_logContext.log(cta::log::INFO, "Tape session started");
//m_logContext.log(cta::log::DEBUG, "Created tapeFile::ReadSession with success");
return rs;
}catch(cta::exception::Exception & ex){
......@@ -216,10 +220,15 @@ void castor::tape::tapeserver::daemon::TapeReadSingleThread::run() {
try{
// Report the parameters of the session to the main thread
typedef cta::log::Param Param;
m_watchdog.addParameter(Param("TPVID", m_volInfo.vid));
m_watchdog.addParameter(Param("vid", m_volInfo.vid));
m_watchdog.addParameter(Param("mountType", mountTypeToString(m_volInfo.mountType)));
m_watchdog.addParameter(Param("mountId", m_volInfo.mountId));
m_watchdog.addParameter(Param("volReqId", m_volInfo.mountId));
m_watchdog.addParameter(Param("vo",m_retrieveMount.getVo()));
m_watchdog.addParameter(Param("mediaType",m_retrieveMount.getMediaType()));
m_watchdog.addParameter(Param("tapePool",m_retrieveMount.getPoolName()));
m_watchdog.addParameter(Param("logicalLibrary",m_drive.config.logicalLibrary));
// Set the tape thread time in the watchdog for total time estimation in case
// of crash
......@@ -336,10 +345,6 @@ void castor::tape::tapeserver::daemon::TapeReadSingleThread::run() {
// The session completed successfully, and the cleaner (unmount) executed
// at the end of the previous block. Log the results.
cta::log::ScopedParamContainer params(m_logContext);
params.add("vo",m_retrieveMount.getVo());
params.add("capacity",m_retrieveMount.getDensity());
params.add("tapePoolName",m_retrieveMount.getPoolName());
params.add("dgn",m_drive.config.logicalLibrary);
params.add("status", "success");
m_stats.totalTime = totalTimer.secs();
logWithStat(cta::log::INFO, "Tape thread complete",
......@@ -383,7 +388,7 @@ void castor::tape::tapeserver::daemon::TapeReadSingleThread::run() {
void castor::tape::tapeserver::daemon::TapeReadSingleThread::logWithStat(
int level, const std::string& msg, cta::log::ScopedParamContainer& params) {
params.add("type", "read")
.add("TPVID", m_volInfo.vid)
.add("vid", m_volInfo.vid)
.add("mountTime", m_stats.mountTime)
.add("positionTime", m_stats.positionTime)
.add("waitInstructionsTime", m_stats.waitInstructionsTime)
......
......@@ -175,10 +175,13 @@ castor::tape::tapeserver::daemon::TapeWriteSingleThread::openWriteSession() {
);
cta::log::ScopedParamContainer params(m_logContext);
params.add("vo",m_archiveMount.getVo());
params.add("capacity",m_archiveMount.getDensity());
params.add("tapePoolName",m_archiveMount.getPoolName());
params.add("dgn",m_drive.config.logicalLibrary);
m_logContext.log(cta::log::INFO, "Tape Write session session successfully started");
params.add("mediaType",m_archiveMount.getMediaType());
params.add("tapePool",m_archiveMount.getPoolName());
params.add("logicalLibrary",m_drive.config.logicalLibrary);
params.add("mountType",mountTypeToString(m_volInfo.mountType));
params.add("vendor",m_archiveMount.getVendor());
params.add("capacityInBytes",m_archiveMount.getCapacityInBytes());
m_logContext.log(cta::log::INFO, "Tape session started");
}
catch (cta::exception::Exception & e) {
ScopedParam sp0(m_logContext, Param("ErrorMessage", e.getMessageValue()));
......@@ -284,11 +287,16 @@ void castor::tape::tapeserver::daemon::TapeWriteSingleThread::run() {
{
// Report the parameters of the session to the main thread
typedef cta::log::Param Param;
m_watchdog.addParameter(Param("TPVID", m_volInfo.vid));
m_watchdog.addParameter(Param("vid", m_volInfo.vid));
m_watchdog.addParameter(Param("mountType", mountTypeToString(m_volInfo.mountType)));
m_watchdog.addParameter(Param("mountId", m_volInfo.mountId));
m_watchdog.addParameter(Param("volReqId", m_volInfo.mountId));
m_watchdog.addParameter(Param("vo",m_archiveMount.getVo()));
m_watchdog.addParameter(Param("mediaType",m_archiveMount.getMediaType()));
m_watchdog.addParameter(Param("tapePool",m_archiveMount.getPoolName()));
m_watchdog.addParameter(Param("logicalLibrary",m_drive.config.logicalLibrary));
// Set the tape thread time in the watchdog for total time estimation in case
// of crash
m_watchdog.updateThreadTimer(totalTimer);
......
Supports Markdown
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