//------------------------------------------------------------------------------ // @file: DevicesCmd.cc // @author: Andreas-Joachim Peters - CERN //------------------------------------------------------------------------------ /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2023 CERN/Switzerland * * * * 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 3 of the License, 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, see .* ************************************************************************/ #include "DevicesCmd.hh" #include "mgm/proc/ProcInterface.hh" #include "mgm/tgc/Constants.hh" #include "mgm/XrdMgmOfs.hh" #include "mgm/Devices.hh" #include "common/Path.hh" #include "mgm/config/IConfigEngine.hh" #include "common/Constants.hh" #include "common/StringTokenizer.hh" #include "common/StringUtils.hh" #include "common/SymKeys.hh" #include "common/table_formatter/TableFormatterBase.hh" #include "common/table_formatter/TableFormatting.hh" EOSMGMNAMESPACE_BEGIN //------------------------------------------------------------------------------ // Method implementing the specific behavior of the command executed by the // asynchronous thread //------------------------------------------------------------------------------ eos::console::ReplyProto DevicesCmd::ProcessRequest() noexcept { eos::console::ReplyProto reply; eos::console::DevicesProto devices = mReqProto.devices(); switch (mReqProto.devices().subcmd_case()) { case eos::console::DevicesProto::kLs: LsSubcmd(devices.ls(), reply); break; default: reply.set_std_err("error: not supported"); reply.set_retc(EINVAL); } return reply; } //---------------------------------------------------------------------------- // Execute ls subcommand //---------------------------------------------------------------------------- void DevicesCmd::LsSubcmd(const eos::console::DevicesProto_LsProto& ls, eos::console::ReplyProto& reply) { using eos::console::DevicesProto; std::string list_format; std::string format; auto format_case = ls.outformat(); Json::Value gjson; if ((format_case == DevicesProto::LsProto::NONE) && WantsJsonOutput()) { format_case = DevicesProto::LsProto::MONITORING; } switch (format_case) { case DevicesProto::LsProto::LISTING: break; case DevicesProto::LsProto::MONITORING: break; default : // NONE break; } std::string std_out; if (ls.refresh()) { // force a new extraction gOFS->DeviceTracker->Extract(); } auto extractionTime = gOFS->DeviceTracker->getExtractionTime(); auto extractionLocalTime = gOFS->DeviceTracker->getLocalExtractionTime(); if (format_case != DevicesProto::LsProto::MONITORING) { std_out += "# "; std_out += extractionLocalTime; std_out += "\n"; } for (auto it = FsView::gFsView.mSpaceView.begin(); it != FsView::gFsView.mSpaceView.end(); ++it) { std::map> driveModelStats; std::map> driveModelSmartStats; double totalcapacity = 0; double totalhours = 0; double totaltbhours = 0; uint64_t totaldrivecount = 0; TableFormatterBase table; std::string space = it->first; if (format_case == DevicesProto::LsProto::MONITORING) { table.SetHeader({ {"key", 0, "os"}, {"space", 5, "os"}, {"id", 5, "l"}, {"model", 0, "-s"}, {"serial", 0, "-s"}, {"type", 0, "-s"}, {"capacity", 0, "l"}, {"rpms", 0, "l"}, {"poweronhours", 0, "l"}, {"temp", 0, "l"}, {"smart", 0, "s"}, {"if", 0, "-s"}, {"rla", 0, "-s"}, {"wc", 0, "-s"} }); } else { table.SetHeader({ {it->first, 12, "+l"}, {"model", 0, "-s"}, {"serial", 0, "-s"}, {"type", 0, "-s"}, {"capacity", 0, "+l"}, {"rpms", 0, "l"}, {"poweron[h]", 0, "l"}, {"temp[degrees]", 0, "l"}, {"S.M.A.R.T", 0, "s"}, {"if", 0, "-s"}, {"rla", 0, "-s"}, {"wc", 0, "-s"} }); } auto jinfo = gOFS->DeviceTracker->getJson(); auto spinfo = gOFS->DeviceTracker->getSpaceMap(); auto sminfo = gOFS->DeviceTracker->getSmartMap(); std::vector smStatus {"OK", "no smartctl", "N/A", "FAILING", "Check", "invalid", "unknown"}; std::vector smHuman {"ok", "noctl", "na", "failing", "check", "inval", "unknown"}; if (!jinfo || !spinfo) { if (!WantsJsonOutput()) { reply.set_std_err("error: not yet availabe - try again"); reply.set_retc(EAGAIN); } else { reply.set_std_err("{ \"errmsg\" : \"not yet available -try again\", \"errc\" : 11 }"); reply.set_retc(EAGAIN); } return ; } for (auto it = jinfo->begin(); it != jinfo->end(); ++it) { std::string smartstatus = "unknown"; auto id = it->first; if (!spinfo->count(id)) { // fs not mapped to a space continue; } if (space != (*spinfo)[id]) { // no in this printout continue; } auto sm = sminfo->find(id); if (sm != sminfo->end()) { smartstatus = sm->second; } Json::Value root; std::string errs; Json::CharReaderBuilder jsonReaderBuilder; std::unique_ptr const reader( jsonReaderBuilder.newCharReader()); const std::string& ojson = it->second; if (reader->parse(ojson.c_str(), ojson.c_str() + ojson.size(), &root, &errs)) { try { std::string model = root.isMember("model_name") ? root["model_name"].asString() : "unknown"; std::replace(model.begin(), model.end(), ' ', ':'); std::string serial = root.isMember("serial_number") ? root["serial_number"].asString() : "unknown"; std::string dtype = (root.isMember("device") && root["device"].isMember("type")) ? root["device"]["type"].asString() : "unknown"; uint64_t capacity = (root.isMember("user_capacity") && root["user_capacity"].isMember("bytes")) ? root["user_capacity"]["bytes"].asUInt64() : 0; uint64_t rpms = (root.isMember("rotation_rate")) ? root["rotation_rate"].asUInt64() : 0; uint64_t powerhours = (root.isMember("power_on_time") && root["power_on_time"].isMember("hours")) ? root["power_on_time"]["hours"].asUInt64() : 0; uint64_t temperature = (root.isMember("temperature") && root["temperature"].isMember("current")) ? root["temperature"]["current"].asUInt64() : 0; std::string ifspeed = (root.isMember("interface_speed") && root["interface_speed"].isMember("max") && root["interface_speed"]["max"].isMember("string")) ? root["interface_speed"]["max"]["string"].asString() : "unknown"; std::replace(ifspeed.begin(), ifspeed.end(), ' ', ':'); string read_lookahead = (root.isMember("read_lookahead")) ? (root["read_lookahead"]["enabled"].asBool() ? "true" : "false") : "unknown"; string write_cache = (root.isMember("write_cache")) ? (root["write_cache"]["enabled"].asBool() ? "true" : "false") : "unknown"; if (model.length()) { driveModelStats[model]["count"]++; totaldrivecount++; driveModelStats[model]["bytes"] += capacity; driveModelStats[model]["hours"] += powerhours; totalcapacity += capacity; totalhours += powerhours; totaltbhours += (capacity * powerhours / 1000000000000.0); if (!driveModelSmartStats.count(model)) { // make sure we have each smart status in the smart stats map for (size_t i = 0; i < smStatus.size(); ++i) { driveModelSmartStats[model][smHuman[i]] = 0; } } for (size_t i = 0; i < smStatus.size(); ++i) { if (sm->second == smStatus[i]) { smartstatus = smHuman[i]; driveModelSmartStats[model][smartstatus]++; break; } } } TableData body; TableRow row; if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back("deviceinfo", "os"); row.emplace_back(space, "s"); } row.emplace_back((unsigned long long)id, "l"); row.emplace_back(model, "-s"); row.emplace_back(serial, "-s"); row.emplace_back(dtype, "-s"); if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back((unsigned long long)capacity, "l", "B"); } else { row.emplace_back((unsigned long long)capacity, "+l", "B"); } row.emplace_back((unsigned long long)rpms, "l"); if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back((unsigned long long)powerhours, "l"); } else { row.emplace_back((unsigned long long)powerhours, "+l", "h"); } row.emplace_back((unsigned long long)temperature, "l"); row.emplace_back(smartstatus, "s"); row.emplace_back(ifspeed, "-s"); row.emplace_back(read_lookahead, "-s"); row.emplace_back(write_cache, "-s"); body.push_back(row); table.AddRows(body); } catch (Json::Exception const&) { std_out += "fatal: json exception has been thrown\n"; eos_static_crit("err=\"catched JSON exception\""); } } gjson["extractiontime"]["timestamp"] = (Json::Value::UInt64)extractionTime; gjson["extractiontime"]["localtime"] = extractionLocalTime; gjson["space"][space]["filesystem"][std::to_string(id)] = root; } if (format_case == DevicesProto::LsProto::LISTING) { std_out += table.GenerateTable(); } if (format_case == DevicesProto::LsProto::MONITORING) { std_out += table.GenerateTable(); } if (true) { // we might add a switch to suppress this output later TableFormatterBase table; TableHeader header; TableData body; if (format_case == DevicesProto::LsProto::MONITORING) { header.push_back(std::make_tuple("key", 0, "os")); header.push_back(std::make_tuple("model", 0, "os")); } else { header.push_back(std::make_tuple("space", 0, "+s")); header.push_back(std::make_tuple("model", 0, "-s")); } auto fst = driveModelStats.begin(); if (fst != driveModelStats.end()) { if (format_case == DevicesProto::LsProto::MONITORING) { header.push_back(std::make_tuple("avg:age:years", 0, "f")); } else { header.push_back(std::make_tuple("avg:age[years]", 0, "f")); } for (auto it = fst->second.begin(); it != fst->second.end(); ++it) { if (format_case == DevicesProto::LsProto::MONITORING) { header.push_back(std::make_tuple(it->first, 0, "l")); } else { header.push_back(std::make_tuple(it->first, 0, "+l")); } } } for (auto i = smHuman.begin(); i != smHuman.end(); ++i) { header.push_back(std::make_tuple(std::string("smrt:") + *i, 0, "os")); } table.SetHeader(header); for (auto it = driveModelStats.begin(); it != driveModelStats.end(); ++it) { double avgage = driveModelStats[it->first]["hours"] / driveModelStats[it->first]["count"] / 24.0 / 365.0; TableRow row; if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back("devicestats", "os"); } else { row.emplace_back(space.c_str(), "s"); } row.emplace_back(it->first, "-s"); row.emplace_back(avgage, "f"); gjson["statistics"][it->first]["avg:age:years"] = avgage; for (auto iit = it->second.begin(); iit != it->second.end(); ++iit) { if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back((unsigned long long)iit->second, "l"); } else { if (iit->first == "bytes") { row.emplace_back((unsigned long long)iit->second, "+l", "B"); } else if (iit->first == "hours") { row.emplace_back((unsigned long long)iit->second, "+l", "h"); } else { row.emplace_back((unsigned long long)iit->second, "+l"); } } gjson["statistics"][it->first][iit->first] = (Json::Value::UInt64)iit->second; } for (size_t i = 0; i < smHuman.size(); ++i) { row.emplace_back((unsigned long long) driveModelSmartStats[it->first][smHuman[i]], "l"); } body.push_back(row); } table.AddRows(body); if (!WantsJsonOutput()) { std_out += table.GenerateTable(); } } if (true) { // we might add a switch to suppress this output later TableFormatterBase table; TableHeader header; TableData body; if (format_case == DevicesProto::LsProto::MONITORING) { header.push_back(std::make_tuple("key", 0, "os")); header.push_back(std::make_tuple("tbyears", 0, "of")); header.push_back(std::make_tuple("driveage", 0, "of")); header.push_back(std::make_tuple("drivehours", 0, "ol")); header.push_back(std::make_tuple("clouddollar-replica", 0, "ol")); header.push_back(std::make_tuple("clouddollar-erasure", 0, "ol")); } else { header.push_back(std::make_tuple("Cost-Matrix", 0, "+s")); header.push_back(std::make_tuple("TB*Years", 0, "+l")); header.push_back(std::make_tuple("Avg-Drive-Hours", 6, "+l")); header.push_back(std::make_tuple("Tot-Drive-Hours", 0, "+l")); header.push_back(std::make_tuple("Cloud$-Replica", 0, "+l")); header.push_back(std::make_tuple("Cloud$-Erasure", 0, "+l")); } table.SetHeader(header); double volyears = totalcapacity * totalhours / 24.0 / 365.0; double tbyears = totaltbhours / 24.0 / 365.0; double tage = totalhours / (totaldrivecount ? totaldrivecount : 1000000); double cloudinstancecost = tbyears * 250.0; // assume 250 cloud$ per tb/year gjson["cost"]["vol:years"] = volyears; gjson["cost"]["tb:years"] = tbyears; gjson["cost"]["avg-drive-hours"] = tage; gjson["cost"]["tot-drive-hours"] = totalhours; gjson["cost"]["cloud-dollar-replica"] = cloudinstancecost / 2.0; gjson["cost"]["cloud-dollar-erasure"] = cloudinstancecost / 1.2; TableRow row; if (format_case == DevicesProto::LsProto::MONITORING) { row.emplace_back(tbyears, "f"); row.emplace_back(tage, "l"); row.emplace_back(totalhours, "l"); row.emplace_back(cloudinstancecost / 2.0, "l"); row.emplace_back(cloudinstancecost / 1.2, "l"); } else { row.emplace_back(gOFS->MgmOfsInstanceName.c_str(), "s"); row.emplace_back(tbyears, "+l"); row.emplace_back(tage, "+l"); row.emplace_back(totalhours, "+l"); row.emplace_back(cloudinstancecost / 2.0, "+l", "$"); row.emplace_back(cloudinstancecost / 1.2, "+l", "$"); } body.push_back(row); table.AddRows(body); if (!WantsJsonOutput()) { std_out += table.GenerateTable(); } } } if (WantsJsonOutput()) { std_out = SSTR(gjson).c_str(); } reply.set_std_out(std_out); reply.set_retc(0); } EOSMGMNAMESPACE_END