140e9b92eSEd Tanous // SPDX-License-Identifier: Apache-2.0 240e9b92eSEd Tanous // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3e21126ecSEd Tanous #pragma once 4e21126ecSEd Tanous 5d7857201SEd Tanous #include "bmcweb_config.h" 6d7857201SEd Tanous 7e21126ecSEd Tanous #include "app.hpp" 8d7857201SEd Tanous #include "async_resp.hpp" 9d7857201SEd Tanous #include "error_messages.hpp" 10e21126ecSEd Tanous #include "generated/enums/log_entry.hpp" 113d66430aSEd Tanous #include "gzfile.hpp" 12d7857201SEd Tanous #include "http_request.hpp" 13d7857201SEd Tanous #include "human_sort.hpp" 14d7857201SEd Tanous #include "logging.hpp" 15e21126ecSEd Tanous #include "query.hpp" 16e21126ecSEd Tanous #include "registries/privilege_registry.hpp" 17d7857201SEd Tanous #include "utils/query_param.hpp" 18e21126ecSEd Tanous 19d7857201SEd Tanous #include <boost/beast/http/verb.hpp> 20d7857201SEd Tanous #include <boost/url/format.hpp> 21d7857201SEd Tanous #include <nlohmann/json.hpp> 22d7857201SEd Tanous 23d7857201SEd Tanous #include <algorithm> 24d7857201SEd Tanous #include <charconv> 25d7857201SEd Tanous #include <cstddef> 26e21126ecSEd Tanous #include <cstdint> 27d7857201SEd Tanous #include <filesystem> 28d7857201SEd Tanous #include <format> 29d7857201SEd Tanous #include <functional> 30e21126ecSEd Tanous #include <memory> 31d7857201SEd Tanous #include <string> 32e21126ecSEd Tanous #include <string_view> 33d7857201SEd Tanous #include <system_error> 34e21126ecSEd Tanous #include <utility> 35e21126ecSEd Tanous #include <vector> 36e21126ecSEd Tanous 37e21126ecSEd Tanous namespace redfish 38e21126ecSEd Tanous { 393d66430aSEd Tanous constexpr const char* hostLoggerFolderPath = "/var/log/console"; 403d66430aSEd Tanous 41*504af5a0SPatrick Williams inline bool getHostLoggerFiles( 42*504af5a0SPatrick Williams const std::string& hostLoggerFilePath, 433d66430aSEd Tanous std::vector<std::filesystem::path>& hostLoggerFiles) 443d66430aSEd Tanous { 453d66430aSEd Tanous std::error_code ec; 463d66430aSEd Tanous std::filesystem::directory_iterator logPath(hostLoggerFilePath, ec); 473d66430aSEd Tanous if (ec) 483d66430aSEd Tanous { 493d66430aSEd Tanous BMCWEB_LOG_WARNING("{}", ec.message()); 503d66430aSEd Tanous return false; 513d66430aSEd Tanous } 523d66430aSEd Tanous for (const std::filesystem::directory_entry& it : logPath) 533d66430aSEd Tanous { 543d66430aSEd Tanous std::string filename = it.path().filename(); 553d66430aSEd Tanous // Prefix of each log files is "log". Find the file and save the 563d66430aSEd Tanous // path 573d66430aSEd Tanous if (filename.starts_with("log")) 583d66430aSEd Tanous { 593d66430aSEd Tanous hostLoggerFiles.emplace_back(it.path()); 603d66430aSEd Tanous } 613d66430aSEd Tanous } 623d66430aSEd Tanous // As the log files rotate, they are appended with a ".#" that is higher for 633d66430aSEd Tanous // the older logs. Since we start from oldest logs, sort the name in 643d66430aSEd Tanous // descending order. 653d66430aSEd Tanous std::sort(hostLoggerFiles.rbegin(), hostLoggerFiles.rend(), 663d66430aSEd Tanous AlphanumLess<std::string>()); 673d66430aSEd Tanous 683d66430aSEd Tanous return true; 693d66430aSEd Tanous } 703d66430aSEd Tanous 713d66430aSEd Tanous inline bool getHostLoggerEntries( 723d66430aSEd Tanous const std::vector<std::filesystem::path>& hostLoggerFiles, uint64_t skip, 733d66430aSEd Tanous uint64_t top, std::vector<std::string>& logEntries, size_t& logCount) 743d66430aSEd Tanous { 753d66430aSEd Tanous GzFileReader logFile; 763d66430aSEd Tanous 773d66430aSEd Tanous // Go though all log files and expose host logs. 783d66430aSEd Tanous for (const std::filesystem::path& it : hostLoggerFiles) 793d66430aSEd Tanous { 803d66430aSEd Tanous if (!logFile.gzGetLines(it.string(), skip, top, logEntries, logCount)) 813d66430aSEd Tanous { 823d66430aSEd Tanous BMCWEB_LOG_ERROR("fail to expose host logs"); 833d66430aSEd Tanous return false; 843d66430aSEd Tanous } 853d66430aSEd Tanous } 863d66430aSEd Tanous // Get lastMessage from constructor by getter 873d66430aSEd Tanous std::string lastMessage = logFile.getLastMessage(); 883d66430aSEd Tanous if (!lastMessage.empty()) 893d66430aSEd Tanous { 903d66430aSEd Tanous logCount++; 913d66430aSEd Tanous if (logCount > skip && logCount <= (skip + top)) 923d66430aSEd Tanous { 933d66430aSEd Tanous logEntries.push_back(lastMessage); 943d66430aSEd Tanous } 953d66430aSEd Tanous } 963d66430aSEd Tanous return true; 973d66430aSEd Tanous } 98e21126ecSEd Tanous 99e21126ecSEd Tanous inline void fillHostLoggerEntryJson(std::string_view logEntryID, 100e21126ecSEd Tanous std::string_view msg, 101e21126ecSEd Tanous nlohmann::json::object_t& logEntryJson) 102e21126ecSEd Tanous { 103e21126ecSEd Tanous // Fill in the log entry with the gathered data. 104e21126ecSEd Tanous logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 105e21126ecSEd Tanous logEntryJson["@odata.id"] = boost::urls::format( 106e21126ecSEd Tanous "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries/{}", 107e21126ecSEd Tanous BMCWEB_REDFISH_SYSTEM_URI_NAME, logEntryID); 108e21126ecSEd Tanous logEntryJson["Name"] = "Host Logger Entry"; 109e21126ecSEd Tanous logEntryJson["Id"] = logEntryID; 110e21126ecSEd Tanous logEntryJson["Message"] = msg; 111e21126ecSEd Tanous logEntryJson["EntryType"] = log_entry::LogEntryType::Oem; 112e21126ecSEd Tanous logEntryJson["Severity"] = log_entry::EventSeverity::OK; 113e21126ecSEd Tanous logEntryJson["OemRecordFormat"] = "Host Logger Entry"; 114e21126ecSEd Tanous } 115e21126ecSEd Tanous 1167945eeedSEd Tanous inline void handleSystemsLogServicesHostloggerGet( 1177945eeedSEd Tanous App& app, const crow::Request& req, 118e21126ecSEd Tanous const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1197945eeedSEd Tanous const std::string& systemName) 1207945eeedSEd Tanous { 121e21126ecSEd Tanous if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 122e21126ecSEd Tanous { 123e21126ecSEd Tanous return; 124e21126ecSEd Tanous } 125e21126ecSEd Tanous if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 126e21126ecSEd Tanous { 127e21126ecSEd Tanous // Option currently returns no systems. TBD 128e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 129e21126ecSEd Tanous systemName); 130e21126ecSEd Tanous return; 131e21126ecSEd Tanous } 132e21126ecSEd Tanous if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 133e21126ecSEd Tanous { 134e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 135e21126ecSEd Tanous systemName); 136e21126ecSEd Tanous return; 137e21126ecSEd Tanous } 138e21126ecSEd Tanous asyncResp->res.jsonValue["@odata.id"] = 139e21126ecSEd Tanous std::format("/redfish/v1/Systems/{}/LogServices/HostLogger", 140e21126ecSEd Tanous BMCWEB_REDFISH_SYSTEM_URI_NAME); 1417945eeedSEd Tanous asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 142e21126ecSEd Tanous asyncResp->res.jsonValue["Name"] = "Host Logger Service"; 143e21126ecSEd Tanous asyncResp->res.jsonValue["Description"] = "Host Logger Service"; 144e21126ecSEd Tanous asyncResp->res.jsonValue["Id"] = "HostLogger"; 1457945eeedSEd Tanous asyncResp->res.jsonValue["Entries"]["@odata.id"] = 1467945eeedSEd Tanous std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 147e21126ecSEd Tanous BMCWEB_REDFISH_SYSTEM_URI_NAME); 148e21126ecSEd Tanous } 149e21126ecSEd Tanous 1507945eeedSEd Tanous inline void handleSystemsLogServicesHostloggerEntriesGet( 1517945eeedSEd Tanous App& app, const crow::Request& req, 152e21126ecSEd Tanous const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1537945eeedSEd Tanous const std::string& systemName) 1547945eeedSEd Tanous { 155e21126ecSEd Tanous query_param::QueryCapabilities capabilities = { 156e21126ecSEd Tanous .canDelegateTop = true, 157e21126ecSEd Tanous .canDelegateSkip = true, 158e21126ecSEd Tanous }; 159e21126ecSEd Tanous query_param::Query delegatedQuery; 1607945eeedSEd Tanous if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 1617945eeedSEd Tanous delegatedQuery, capabilities)) 162e21126ecSEd Tanous { 163e21126ecSEd Tanous return; 164e21126ecSEd Tanous } 165e21126ecSEd Tanous if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 166e21126ecSEd Tanous { 167e21126ecSEd Tanous // Option currently returns no systems. TBD 168e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 169e21126ecSEd Tanous systemName); 170e21126ecSEd Tanous return; 171e21126ecSEd Tanous } 172e21126ecSEd Tanous if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 173e21126ecSEd Tanous { 174e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 175e21126ecSEd Tanous systemName); 176e21126ecSEd Tanous return; 177e21126ecSEd Tanous } 1787945eeedSEd Tanous asyncResp->res.jsonValue["@odata.id"] = 1797945eeedSEd Tanous std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 180e21126ecSEd Tanous BMCWEB_REDFISH_SYSTEM_URI_NAME); 181e21126ecSEd Tanous asyncResp->res.jsonValue["@odata.type"] = 182e21126ecSEd Tanous "#LogEntryCollection.LogEntryCollection"; 183e21126ecSEd Tanous asyncResp->res.jsonValue["Name"] = "HostLogger Entries"; 184e21126ecSEd Tanous asyncResp->res.jsonValue["Description"] = 185e21126ecSEd Tanous "Collection of HostLogger Entries"; 186e21126ecSEd Tanous nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 187e21126ecSEd Tanous logEntryArray = nlohmann::json::array(); 188e21126ecSEd Tanous asyncResp->res.jsonValue["Members@odata.count"] = 0; 189e21126ecSEd Tanous 190e21126ecSEd Tanous std::vector<std::filesystem::path> hostLoggerFiles; 191e21126ecSEd Tanous if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 192e21126ecSEd Tanous { 193e21126ecSEd Tanous BMCWEB_LOG_DEBUG("Failed to get host log file path"); 194e21126ecSEd Tanous return; 195e21126ecSEd Tanous } 196e21126ecSEd Tanous // If we weren't provided top and skip limits, use the defaults. 197e21126ecSEd Tanous size_t skip = delegatedQuery.skip.value_or(0); 1987945eeedSEd Tanous size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 199e21126ecSEd Tanous size_t logCount = 0; 200e21126ecSEd Tanous // This vector only store the entries we want to expose that 201e21126ecSEd Tanous // control by skip and top. 202e21126ecSEd Tanous std::vector<std::string> logEntries; 2037945eeedSEd Tanous if (!getHostLoggerEntries(hostLoggerFiles, skip, top, logEntries, logCount)) 204e21126ecSEd Tanous { 205e21126ecSEd Tanous messages::internalError(asyncResp->res); 206e21126ecSEd Tanous return; 207e21126ecSEd Tanous } 208e21126ecSEd Tanous // If vector is empty, that means skip value larger than total 209e21126ecSEd Tanous // log count 210e21126ecSEd Tanous if (logEntries.empty()) 211e21126ecSEd Tanous { 212e21126ecSEd Tanous asyncResp->res.jsonValue["Members@odata.count"] = logCount; 213e21126ecSEd Tanous return; 214e21126ecSEd Tanous } 215e21126ecSEd Tanous if (!logEntries.empty()) 216e21126ecSEd Tanous { 217e21126ecSEd Tanous for (size_t i = 0; i < logEntries.size(); i++) 218e21126ecSEd Tanous { 219e21126ecSEd Tanous nlohmann::json::object_t hostLogEntry; 2207945eeedSEd Tanous fillHostLoggerEntryJson(std::to_string(skip + i), logEntries[i], 2217945eeedSEd Tanous hostLogEntry); 222e21126ecSEd Tanous logEntryArray.emplace_back(std::move(hostLogEntry)); 223e21126ecSEd Tanous } 224e21126ecSEd Tanous 225e21126ecSEd Tanous asyncResp->res.jsonValue["Members@odata.count"] = logCount; 226e21126ecSEd Tanous if (skip + top < logCount) 227e21126ecSEd Tanous { 228e21126ecSEd Tanous asyncResp->res.jsonValue["Members@odata.nextLink"] = 229e21126ecSEd Tanous std::format( 230e21126ecSEd Tanous "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries?$skip=", 231e21126ecSEd Tanous BMCWEB_REDFISH_SYSTEM_URI_NAME) + 232e21126ecSEd Tanous std::to_string(skip + top); 233e21126ecSEd Tanous } 234e21126ecSEd Tanous } 235e21126ecSEd Tanous } 236e21126ecSEd Tanous 2377945eeedSEd Tanous inline void handleSystemsLogServicesHostloggerEntriesEntryGet( 2387945eeedSEd Tanous App& app, const crow::Request& req, 239e21126ecSEd Tanous const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 2407945eeedSEd Tanous const std::string& systemName, const std::string& param) 2417945eeedSEd Tanous { 242e21126ecSEd Tanous if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 243e21126ecSEd Tanous { 244e21126ecSEd Tanous return; 245e21126ecSEd Tanous } 246e21126ecSEd Tanous if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 247e21126ecSEd Tanous { 248e21126ecSEd Tanous // Option currently returns no systems. TBD 249e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 250e21126ecSEd Tanous systemName); 251e21126ecSEd Tanous return; 252e21126ecSEd Tanous } 253e21126ecSEd Tanous if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 254e21126ecSEd Tanous { 255e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "ComputerSystem", 256e21126ecSEd Tanous systemName); 257e21126ecSEd Tanous return; 258e21126ecSEd Tanous } 259e21126ecSEd Tanous std::string_view targetID = param; 260e21126ecSEd Tanous 261e21126ecSEd Tanous uint64_t idInt = 0; 262e21126ecSEd Tanous 2637945eeedSEd Tanous auto [ptr, ec] = std::from_chars(targetID.begin(), targetID.end(), idInt); 264e21126ecSEd Tanous if (ec != std::errc{} || ptr != targetID.end()) 265e21126ecSEd Tanous { 2667945eeedSEd Tanous messages::resourceNotFound(asyncResp->res, "LogEntry", param); 267e21126ecSEd Tanous return; 268e21126ecSEd Tanous } 269e21126ecSEd Tanous 270e21126ecSEd Tanous std::vector<std::filesystem::path> hostLoggerFiles; 271e21126ecSEd Tanous if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 272e21126ecSEd Tanous { 273e21126ecSEd Tanous BMCWEB_LOG_DEBUG("Failed to get host log file path"); 274e21126ecSEd Tanous return; 275e21126ecSEd Tanous } 276e21126ecSEd Tanous 277e21126ecSEd Tanous size_t logCount = 0; 278e21126ecSEd Tanous size_t top = 1; 279e21126ecSEd Tanous std::vector<std::string> logEntries; 280e21126ecSEd Tanous // We can get specific entry by skip and top. For example, if we 281e21126ecSEd Tanous // want to get nth entry, we can set skip = n-1 and top = 1 to 282e21126ecSEd Tanous // get that entry 2837945eeedSEd Tanous if (!getHostLoggerEntries(hostLoggerFiles, idInt, top, logEntries, 2847945eeedSEd Tanous logCount)) 285e21126ecSEd Tanous { 286e21126ecSEd Tanous messages::internalError(asyncResp->res); 287e21126ecSEd Tanous return; 288e21126ecSEd Tanous } 289e21126ecSEd Tanous 290e21126ecSEd Tanous if (!logEntries.empty()) 291e21126ecSEd Tanous { 292e21126ecSEd Tanous nlohmann::json::object_t hostLogEntry; 2937945eeedSEd Tanous fillHostLoggerEntryJson(targetID, logEntries[0], hostLogEntry); 294e21126ecSEd Tanous asyncResp->res.jsonValue.update(hostLogEntry); 295e21126ecSEd Tanous return; 296e21126ecSEd Tanous } 297e21126ecSEd Tanous 298e21126ecSEd Tanous // Requested ID was not found 299e21126ecSEd Tanous messages::resourceNotFound(asyncResp->res, "LogEntry", param); 300e21126ecSEd Tanous } 301e21126ecSEd Tanous 302e21126ecSEd Tanous inline void requestRoutesSystemsLogServiceHostlogger(App& app) 303e21126ecSEd Tanous { 3047945eeedSEd Tanous BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/") 3057945eeedSEd Tanous .privileges(redfish::privileges::getLogService) 3067945eeedSEd Tanous .methods(boost::beast::http::verb::get)(std::bind_front( 3077945eeedSEd Tanous handleSystemsLogServicesHostloggerGet, std::ref(app))); 3087945eeedSEd Tanous BMCWEB_ROUTE(app, 3097945eeedSEd Tanous "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/") 3103d45ef66SGunnar Mills .privileges(redfish::privileges::getLogEntryCollection) 3117945eeedSEd Tanous .methods(boost::beast::http::verb::get)(std::bind_front( 3127945eeedSEd Tanous handleSystemsLogServicesHostloggerEntriesGet, std::ref(app))); 3137945eeedSEd Tanous 3147945eeedSEd Tanous BMCWEB_ROUTE( 3157945eeedSEd Tanous app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/<str>/") 3167945eeedSEd Tanous .privileges(redfish::privileges::getLogEntry) 3177945eeedSEd Tanous .methods(boost::beast::http::verb::get)(std::bind_front( 3187945eeedSEd Tanous handleSystemsLogServicesHostloggerEntriesEntryGet, std::ref(app))); 319e21126ecSEd Tanous } 320e21126ecSEd Tanous 321e21126ecSEd Tanous } // namespace redfish 322