Commit ad01fe98 authored by Cedric Caffy's avatar Cedric Caffy
Browse files

Catalogue::getCachedVirtualOrganizationByTapepool() + VO added to the...

Catalogue::getCachedVirtualOrganizationByTapepool() + VO added to the DriveState object + VO and read/write max drives added to the getQueuesAndMountSummaries method

- Objectstore definition modified : there is no maxDrivesAllowed item
  anymore
parent 19263ab7
......@@ -60,7 +60,7 @@ RdbmsCatalogue::RdbmsCatalogue(
m_groupMountPolicyCache(10),
m_userMountPolicyCache(10),
m_allMountPoliciesCache(60),
m_tapepoolVirtualOrganizationCache(120),
m_tapepoolVirtualOrganizationCache(60),
m_expectedNbArchiveRoutesCache(10),
m_isAdminCache(10),
m_activitiesFairShareWeights(10) {}
......
......@@ -125,7 +125,7 @@ struct TapePool {
* number of physical files stored in the tape pool containing that tape.
*/
uint64_t nbPhysicalFiles;
/**
* Optional value used by the tape pool supply mechanism.
*/
......
......@@ -225,6 +225,7 @@ void TextFormatter::printDriveLsHeader() {
"since",
"vid",
"tapepool",
"vo",
"files",
"data",
"MB/s",
......@@ -283,6 +284,7 @@ void TextFormatter::print(const DriveLsItem &drls_item)
driveStatusSince,
drls_item.vid(),
drls_item.tapepool(),
drls_item.vo(),
filesTransferredInSession,
bytesTransferredInSession,
latestBandwidth,
......@@ -676,6 +678,7 @@ void TextFormatter::printShowQueuesHeader() {
push_back(
"type",
"tapepool",
"vo",
"logical library",
"vid",
"files queued",
......@@ -683,7 +686,8 @@ void TextFormatter::printShowQueuesHeader() {
"oldest age",
"priority",
"min age",
"max drives",
"read max drives",
"write max drives",
"cur. mounts",
"cur. files",
"cur. data",
......@@ -702,18 +706,21 @@ void TextFormatter::printShowQueuesHeader() {
void TextFormatter::print(const ShowQueuesItem &sq_item) {
std::string priority;
std::string minAge;
std::string maxDrivesAllowed;
std::string readMaxDrives;
std::string writeMaxDrives;
if(sq_item.mount_type() == ARCHIVE_FOR_USER || sq_item.mount_type() == ARCHIVE_FOR_REPACK ||
sq_item.mount_type() == RETRIEVE) {
priority = std::to_string(sq_item.priority());
minAge = std::to_string(sq_item.min_age());
maxDrivesAllowed = std::to_string(sq_item.max_drives());
readMaxDrives = std::to_string(sq_item.read_max_drives());
writeMaxDrives = std::to_string(sq_item.write_max_drives());
}
push_back(
toString(ProtobufToMountType(sq_item.mount_type())),
sq_item.tapepool(),
sq_item.vo(),
sq_item.logical_library(),
sq_item.vid(),
sq_item.queued_files(),
......@@ -721,7 +728,8 @@ void TextFormatter::print(const ShowQueuesItem &sq_item) {
sq_item.oldest_age(),
priority,
minAge,
maxDrivesAllowed,
readMaxDrives,
writeMaxDrives,
sq_item.cur_mounts(),
sq_item.cur_files(),
dataSizeToStr(sq_item.cur_bytes()),
......
......@@ -52,6 +52,7 @@ bool DriveState::operator==(const DriveState &rhs) const {
&& desiredDriveState==rhs.desiredDriveState
&& currentVid==rhs.currentVid
&& currentTapePool==rhs.currentTapePool
&& currentVo == rhs.currentVo
&& currentPriority == rhs.currentPriority
&& bool(currentActivityAndWeight) == bool(rhs.currentActivityAndWeight)
&& (currentActivityAndWeight? (
......@@ -62,6 +63,7 @@ bool DriveState::operator==(const DriveState &rhs) const {
): true)
&& nextMountType == rhs.nextMountType
&& nextTapepool == rhs.nextTapepool
&& nextVo == rhs.nextVo
&& nextVid == rhs.nextVid
&& bool(nextActivityAndWeight) == bool(rhs.nextActivityAndWeight)
&& (nextActivityAndWeight? (
......@@ -108,6 +110,7 @@ std::ostream &operator<<(std::ostream &os, const DriveState &obj) {
<< " desiredState=" << obj.desiredDriveState
<< " currentVid=" << obj.currentVid
<< " currentTapePool=" << obj.currentTapePool
<< " currentVo=" << obj.currentVo
<< " currentPriority=" << obj.currentPriority
<< " currentActivity=";
if (obj.currentActivityAndWeight) {
......@@ -119,6 +122,7 @@ std::ostream &operator<<(std::ostream &os, const DriveState &obj) {
os << " nextMountType=" << obj.nextMountType
<< " nextVid=" << obj.nextVid
<< " nextTapePool=" << obj.nextTapepool
<< " nextVo=" << obj.nextVo
<< " currentNext=";
if (obj.nextActivityAndWeight) {
os << "(" << obj.nextActivityAndWeight.value().activity
......
......@@ -73,6 +73,7 @@ struct DriveState {
DesiredDriveState desiredDriveState;
std::string currentVid;
std::string currentTapePool;
std::string currentVo;
uint64_t currentPriority = 0;
struct ActivityAndWeight {
std::string activity;
......@@ -82,6 +83,7 @@ struct DriveState {
MountType nextMountType = MountType::NoMount;
std::string nextVid;
std::string nextTapepool;
std::string nextVo;
uint64_t nextPriority = 0;
optional<ActivityAndWeight> nextActivityAndWeight;
std::vector<DriveConfigItem> driveConfigItems;
......
......@@ -33,6 +33,9 @@ namespace dataStructures {
struct QueueAndMountSummary {
MountType mountType=MountType::NoMount;
std::string tapePool;
std::string vo;
uint64_t readMaxDrives;
uint64_t writeMaxDrives;
std::string vid;
std::string logicalLibrary;
uint64_t filesQueued=0;
......
......@@ -108,6 +108,7 @@ cta::common::dataStructures::DriveState DriveState::getState() {
ret.desiredDriveState.forceDown = m_payload.desiredforcedown();
ret.currentVid = m_payload.currentvid();
ret.currentTapePool = m_payload.currenttapepool();
ret.currentVo = m_payload.current_vo();
ret.currentPriority = m_payload.current_priority();
ret.ctaVersion = m_payload.cta_version();
if(m_payload.has_reason()){
......@@ -129,6 +130,8 @@ cta::common::dataStructures::DriveState DriveState::getState() {
ret.nextMountType = (common::dataStructures::MountType) m_payload.nextmounttype();
if (m_payload.has_nexttapepool())
ret.nextTapepool = m_payload.nexttapepool();
if(m_payload.has_next_vo())
ret.nextVo = m_payload.next_vo();
if (m_payload.has_nextvid())
ret.nextVid = m_payload.nextvid();
if (m_payload.has_next_priority())
......@@ -170,6 +173,7 @@ void DriveState::setState(cta::common::dataStructures::DriveState& state) {
m_payload.set_desiredforcedown(desiredDriveState.forceDown);
m_payload.set_currentvid(state.currentVid);
m_payload.set_currenttapepool(state.currentTapePool);
m_payload.set_current_vo(state.currentVo);
m_payload.set_current_priority(state.currentPriority);
cta::optional<std::string> reason = desiredDriveState.reason;
cta::optional<std::string> comment = desiredDriveState.comment;
......@@ -193,6 +197,7 @@ void DriveState::setState(cta::common::dataStructures::DriveState& state) {
}
m_payload.set_nextvid(state.nextVid);
m_payload.set_nexttapepool(state.nextTapepool);
m_payload.set_next_vo(state.nextVo);
m_payload.set_next_priority(state.nextPriority);
m_payload.set_nextmounttype((uint32_t)state.nextMountType);
if (state.nextActivityAndWeight) {
......
......@@ -266,6 +266,8 @@ message DriveState {
repeated DriveConfig drive_config = 5038;
optional string reason = 5039;
optional string comment = 5040;
optional string current_vo = 5041;
optional string next_vo = 5042;
// TODO: implement or remove required EntryLog creationlog = 5023;
}
......
......@@ -489,6 +489,7 @@ void OStoreDB::fetchMountInfo(SchedulerDatabase::TapeMountDecisionInfo& tmdi, Ro
tmdi.existingOrNextMounts.push_back(ExistingMount());
tmdi.existingOrNextMounts.back().type = d.mountType;
tmdi.existingOrNextMounts.back().tapePool = d.currentTapePool;
tmdi.existingOrNextMounts.back().vo = d.currentVo;
tmdi.existingOrNextMounts.back().driveName = d.driveName;
tmdi.existingOrNextMounts.back().vid = d.currentVid;
tmdi.existingOrNextMounts.back().currentMount = true;
......@@ -502,6 +503,7 @@ void OStoreDB::fetchMountInfo(SchedulerDatabase::TapeMountDecisionInfo& tmdi, Ro
tmdi.existingOrNextMounts.push_back(ExistingMount());
tmdi.existingOrNextMounts.back().type = d.nextMountType;
tmdi.existingOrNextMounts.back().tapePool = d.nextTapepool;
tmdi.existingOrNextMounts.back().vo = d.nextVo;
tmdi.existingOrNextMounts.back().driveName = d.driveName;
tmdi.existingOrNextMounts.back().vid = d.nextVid;
tmdi.existingOrNextMounts.back().currentMount = false;
......@@ -3131,7 +3133,7 @@ void OStoreDB::reportDriveStatus(const common::dataStructures::DriveInfo& driveI
cta::common::dataStructures::MountType mountType, common::dataStructures::DriveStatus status,
time_t reportTime, log::LogContext & lc, uint64_t mountSessionId, uint64_t byteTransferred,
uint64_t filesTransferred, double latestBandwidth, const std::string& vid,
const std::string& tapepool) {
const std::string& tapepool, const std::string & vo) {
using common::dataStructures::DriveStatus;
// Wrap all the parameters together for easier manipulation by sub-functions
ReportDriveStatusInputs inputs;
......@@ -3144,6 +3146,7 @@ void OStoreDB::reportDriveStatus(const common::dataStructures::DriveInfo& driveI
inputs.status = status;
inputs.vid = vid;
inputs.tapepool = tapepool;
inputs.vo = vo;
updateDriveStatus(driveInfo, inputs, lc);
}
......@@ -3338,6 +3341,7 @@ void OStoreDB::setDriveDown(common::dataStructures::DriveState & driveState,
driveState.desiredDriveState.forceDown=false;
driveState.currentVid="";
driveState.currentTapePool="";
driveState.currentVo = "";
driveState.currentActivityAndWeight = nullopt;
driveState.desiredDriveState.reason = inputs.reason;
}
......@@ -3379,6 +3383,7 @@ void OStoreDB::setDriveUpOrMaybeDown(common::dataStructures::DriveState & driveS
driveState.driveStatus=targetStatus;
driveState.currentVid="";
driveState.currentTapePool="";
driveState.currentVo = "";
driveState.currentActivityAndWeight = nullopt;
}
......@@ -3413,6 +3418,7 @@ void OStoreDB::setDriveProbing(common::dataStructures::DriveState & driveState,
driveState.driveStatus=inputs.status;
driveState.currentVid="";
driveState.currentTapePool="";
driveState.currentVo = "";
driveState.currentActivityAndWeight = nullopt;
}
......@@ -3447,6 +3453,7 @@ void OStoreDB::setDriveStarting(common::dataStructures::DriveState & driveState,
driveState.driveStatus=common::dataStructures::DriveStatus::Starting;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo=inputs.vo;
if (inputs.activityAndWeigh) {
common::dataStructures::DriveState::ActivityAndWeight aaw;
aaw.activity = inputs.activityAndWeigh.value().activity;
......@@ -3486,6 +3493,7 @@ void OStoreDB::setDriveMounting(common::dataStructures::DriveState & driveState,
driveState.driveStatus=common::dataStructures::DriveStatus::Mounting;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3520,6 +3528,7 @@ void OStoreDB::setDriveTransferring(common::dataStructures::DriveState & driveSt
driveState.driveStatus=common::dataStructures::DriveStatus::Transferring;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3552,6 +3561,7 @@ void OStoreDB::setDriveUnloading(common::dataStructures::DriveState & driveState
driveState.driveStatus=common::dataStructures::DriveStatus::Unloading;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3584,6 +3594,7 @@ void OStoreDB::setDriveUnmounting(common::dataStructures::DriveState & driveStat
driveState.driveStatus=common::dataStructures::DriveStatus::Unmounting;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3616,6 +3627,7 @@ void OStoreDB::setDriveDrainingToDisk(common::dataStructures::DriveState & drive
driveState.driveStatus=common::dataStructures::DriveStatus::DrainingToDisk;
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3649,6 +3661,7 @@ void OStoreDB::setDriveCleaningUp(common::dataStructures::DriveState & driveStat
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentActivityAndWeight = nullopt;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
......@@ -3682,6 +3695,7 @@ void OStoreDB::setDriveShutdown(common::dataStructures::DriveState & driveState,
driveState.currentVid=inputs.vid;
driveState.currentTapePool=inputs.tapepool;
driveState.currentActivityAndWeight = nullopt;
driveState.currentVo = inputs.vo;
}
//------------------------------------------------------------------------------
// OStoreDB::TapeMountDecisionInfo::createArchiveMount()
......@@ -3749,6 +3763,7 @@ std::unique_ptr<SchedulerDatabase::ArchiveMount>
inputs.status = common::dataStructures::DriveStatus::Starting;
inputs.vid = tape.vid;
inputs.tapepool = tape.tapePool;
inputs.vo = am.mountInfo.vo;
log::LogContext lc(m_oStoreDB.m_logger);
m_oStoreDB.updateDriveStatus(driveInfo, inputs, lc);
}
......@@ -3823,6 +3838,7 @@ std::unique_ptr<SchedulerDatabase::RetrieveMount>
inputs.status = common::dataStructures::DriveStatus::Starting;
inputs.vid = rm.mountInfo.vid;
inputs.tapepool = rm.mountInfo.tapePool;
inputs.vo = rm.mountInfo.vo;
inputs.activityAndWeigh = activityAndWeight;
log::LogContext lc(m_oStoreDB.m_logger);
m_oStoreDB.updateDriveStatus(driveInfo, inputs, lc);
......@@ -3934,6 +3950,7 @@ void OStoreDB::ArchiveMount::complete(time_t completionTime) {
inputs.status = common::dataStructures::DriveStatus::Up;
inputs.vid = mountInfo.vid;
inputs.tapepool = mountInfo.tapePool;
inputs.vo = mountInfo.vo;
log::LogContext lc(m_oStoreDB.m_logger);
m_oStoreDB.updateDriveStatus(driveInfo, inputs, lc);
}
......@@ -4229,6 +4246,7 @@ void OStoreDB::RetrieveMount::complete(time_t completionTime) {
inputs.status = common::dataStructures::DriveStatus::Up;
inputs.vid = mountInfo.vid;
inputs.tapepool = mountInfo.tapePool;
inputs.vo = mountInfo.vo;
log::LogContext lc(m_oStoreDB.m_logger);
m_oStoreDB.updateDriveStatus(driveInfo, inputs, lc);
}
......@@ -4250,6 +4268,7 @@ void OStoreDB::RetrieveMount::setDriveStatus(cta::common::dataStructures::DriveS
inputs.status = status;
inputs.vid = mountInfo.vid;
inputs.tapepool = mountInfo.tapePool;
inputs.vo = mountInfo.vo;
inputs.reason = reason;
// TODO: statistics!
inputs.byteTransferred = 0;
......@@ -4459,6 +4478,7 @@ void OStoreDB::ArchiveMount::setDriveStatus(cta::common::dataStructures::DriveSt
inputs.status = status;
inputs.vid = mountInfo.vid;
inputs.tapepool = mountInfo.tapePool;
inputs.vo = mountInfo.vo;
inputs.reason = reason;
// TODO: statistics!
inputs.byteTransferred = 0;
......
......@@ -585,7 +585,7 @@ public:
void reportDriveStatus(const common::dataStructures::DriveInfo& driveInfo, cta::common::dataStructures::MountType mountType,
common::dataStructures::DriveStatus status, time_t reportTime, log::LogContext & lc, uint64_t mountSessionId, uint64_t byteTransfered,
uint64_t filesTransfered, double latestBandwidth, const std::string& vid, const std::string& tapepool) override;
uint64_t filesTransfered, double latestBandwidth, const std::string& vid, const std::string& tapepool, const std::string & vo) override;
void reportDriveConfig(const cta::tape::daemon::TpconfigLine& tpConfigLine, const cta::tape::daemon::TapedConfiguration& tapedConfig,log::LogContext& lc) override;
......@@ -610,6 +610,7 @@ private:
double latestBandwidth;
std::string vid;
std::string tapepool;
std::string vo;
optional<common::dataStructures::DriveState::ActivityAndWeight> activityAndWeigh;
optional<std::string> reason;
};
......
......@@ -275,9 +275,9 @@ public:
void reportDriveStatus(const common::dataStructures::DriveInfo& driveInfo, cta::common::dataStructures::MountType mountType,
common::dataStructures::DriveStatus status, time_t reportTime, log::LogContext& lc, uint64_t mountSessionId,
uint64_t byteTransfered, uint64_t filesTransfered, double latestBandwidth, const std::string& vid, const std::string& tapepool) override {
uint64_t byteTransfered, uint64_t filesTransfered, double latestBandwidth, const std::string& vid, const std::string& tapepool, const std::string & vo) override {
m_OStoreDB.reportDriveStatus(driveInfo, mountType, status, reportTime, lc, mountSessionId, byteTransfered, filesTransfered,
latestBandwidth, vid, tapepool);
latestBandwidth, vid, tapepool,vo);
}
void reportDriveConfig(const cta::tape::daemon::TpconfigLine& tpConfigLine, const cta::tape::daemon::TapedConfiguration& tapedConfig,log::LogContext& lc) override {
......
......@@ -1713,13 +1713,20 @@ std::list<common::dataStructures::QueueAndMountSummary> Scheduler::getQueuesAndM
mountDecisionInfo.reset();
double catalogueGetTapePoolTotalTime = 0.0;
double catalogueGetTapesTotalTime = 0.0;
// Add the tape information where useful (archive queues).
double catalogueGetVoTotalTime = 0.0;
// Add the tape and VO information where useful (archive queues).
for (auto & mountOrQueue: ret) {
if (common::dataStructures::MountType::ArchiveForUser==mountOrQueue.mountType || common::dataStructures::MountType::ArchiveForRepack==mountOrQueue.mountType) {
utils::Timer catalogueGetTapePoolTimer;
const auto tapePool = m_catalogue.getTapePool(mountOrQueue.tapePool);
catalogueGetTapePoolTotalTime += catalogueGetTapePoolTimer.secs();
if (tapePool) {
utils::Timer catalogueGetVoTimer;
const auto vo = m_catalogue.getCachedVirtualOrganizationOfTapepool(tapePool->name);
catalogueGetVoTotalTime += catalogueGetVoTimer.secs();
mountOrQueue.vo = vo.name;
mountOrQueue.readMaxDrives = vo.readMaxDrives;
mountOrQueue.writeMaxDrives = vo.writeMaxDrives;
mountOrQueue.tapesCapacity = tapePool->capacityBytes;
mountOrQueue.filesOnTapes = tapePool->nbPhysicalFiles;
mountOrQueue.dataOnTapes = tapePool->dataBytes;
......@@ -1739,6 +1746,12 @@ std::list<common::dataStructures::QueueAndMountSummary> Scheduler::getQueuesAndM
throw cta::exception::Exception("In Scheduler::getQueuesAndMountSummaries(): got unexpected number of tapes from catalogue for a retrieve.");
}
auto &t=tapes.front();
utils::Timer catalogueGetVoTimer;
const auto vo = m_catalogue.getCachedVirtualOrganizationOfTapepool(t.tapePoolName);
catalogueGetVoTotalTime += catalogueGetVoTimer.secs();
mountOrQueue.vo = vo.name;
mountOrQueue.readMaxDrives = vo.readMaxDrives;
mountOrQueue.writeMaxDrives = vo.writeMaxDrives;
mountOrQueue.tapesCapacity += t.capacityInBytes;
mountOrQueue.filesOnTapes += t.lastFSeq;
mountOrQueue.dataOnTapes += t.dataOnTapeInBytes;
......@@ -1754,6 +1767,7 @@ std::list<common::dataStructures::QueueAndMountSummary> Scheduler::getQueuesAndM
spc.add("catalogueVidToLogicalLibraryTime", catalogueVidToLogicalLibraryTime)
.add("schedulerDbTime", schedulerDbTime)
.add("catalogueGetTapePoolTotalTime", catalogueGetTapePoolTotalTime)
.add("catalogueGetVoTotalTime",catalogueGetVoTotalTime)
.add("catalogueGetTapesTotalTime", catalogueGetTapesTotalTime);
lc.log(log::INFO, "In Scheduler::getQueuesAndMountSummaries(): success.");
return ret;
......
......@@ -686,6 +686,7 @@ public:
std::string driveName;
cta::common::dataStructures::MountType type;
std::string tapePool;
std::string vo;
std::string vid;
bool currentMount; ///< True if the mount is current (othermise, it's a next mount).
uint64_t bytesTransferred;
......@@ -809,6 +810,7 @@ public:
* @param latestBandwidth (optional, required by some statuses).
* @param vid (optional, required by some statuses).
* @param tapepool (optional, required by some statuses).
* @param vo (virtual organization, optional, required by some statuses).
*/
virtual void reportDriveStatus (const common::dataStructures::DriveInfo & driveInfo,
cta::common::dataStructures::MountType mountType,
......@@ -820,7 +822,8 @@ public:
uint64_t filesTransfered = std::numeric_limits<uint64_t>::max(),
double latestBandwidth = std::numeric_limits<double>::max(),
const std::string & vid = "",
const std::string & tapepool = "") = 0;
const std::string & tapepool = "",
const std::string & vo = "") = 0;
virtual void reportDriveConfig(const cta::tape::daemon::TpconfigLine& tpConfigLine, const cta::tape::daemon::TapedConfiguration& tapedConfig,log::LogContext& lc) = 0;
......
......@@ -115,6 +115,7 @@ int DriveLsStream::fillBuffer(XrdSsiPb::OStreamBuffer<Data> *streambuf) {
dr_item->set_drive_status(DriveStatusToProtobuf(dr.driveStatus));
dr_item->set_vid(dr.currentVid);
dr_item->set_tapepool(dr.currentTapePool);
dr_item->set_vo(dr.currentVo);
dr_item->set_files_transferred_in_session(dr.filesTransferredInSession);
dr_item->set_bytes_transferred_in_session(dr.bytesTransferredInSession);
dr_item->set_latest_bandwidth(dr.latestBandwidth);
......
......@@ -113,6 +113,9 @@ int ShowQueuesStream::fillBuffer(XrdSsiPb::OStreamBuffer<Data> *streambuf) {
sq_item->set_empty_tapes(sq.emptyTapes);
sq_item->set_disabled_tapes(sq.disabledTapes);
sq_item->set_writable_tapes(sq.writableTapes);
sq_item->set_vo(sq.vo);
sq_item->set_read_max_drives(sq.readMaxDrives);
sq_item->set_write_max_drives(sq.writeMaxDrives);
if (sq.sleepForSpaceInfo) {
sq_item->set_sleeping_for_space(true);
sq_item->set_sleep_start_time(sq.sleepForSpaceInfo.value().startTime);
......
Subproject commit 511cc4ae58d813d94bc531302f4f3ae0af172115
Subproject commit 737c710bde9e1e0bd5a54fa69159dcd525b62ef7
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