1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "bmcweb_config.h" 6 7 #include "app.hpp" 8 #include "async_resp.hpp" 9 #include "error_messages.hpp" 10 #include "generated/enums/log_entry.hpp" 11 #include "gzfile.hpp" 12 #include "http_request.hpp" 13 #include "human_sort.hpp" 14 #include "logging.hpp" 15 #include "query.hpp" 16 #include "registries/privilege_registry.hpp" 17 #include "utils/query_param.hpp" 18 19 #include <boost/beast/http/verb.hpp> 20 #include <boost/url/format.hpp> 21 #include <nlohmann/json.hpp> 22 23 #include <algorithm> 24 #include <charconv> 25 #include <cstddef> 26 #include <cstdint> 27 #include <filesystem> 28 #include <format> 29 #include <functional> 30 #include <memory> 31 #include <string> 32 #include <string_view> 33 #include <system_error> 34 #include <utility> 35 #include <vector> 36 37 namespace redfish 38 { 39 constexpr const char* hostLoggerFolderPath = "/var/log/console"; 40 getHostLoggerFiles(const std::string & hostLoggerFilePath,std::vector<std::filesystem::path> & hostLoggerFiles)41 inline bool getHostLoggerFiles( 42 const std::string& hostLoggerFilePath, 43 std::vector<std::filesystem::path>& hostLoggerFiles) 44 { 45 std::error_code ec; 46 std::filesystem::directory_iterator logPath(hostLoggerFilePath, ec); 47 if (ec) 48 { 49 BMCWEB_LOG_WARNING("{}", ec.message()); 50 return false; 51 } 52 for (const std::filesystem::directory_entry& it : logPath) 53 { 54 std::string filename = it.path().filename(); 55 // Prefix of each log files is "log". Find the file and save the 56 // path 57 if (filename.starts_with("log")) 58 { 59 hostLoggerFiles.emplace_back(it.path()); 60 } 61 } 62 // As the log files rotate, they are appended with a ".#" that is higher for 63 // the older logs. Since we start from oldest logs, sort the name in 64 // descending order. 65 std::sort(hostLoggerFiles.rbegin(), hostLoggerFiles.rend(), 66 AlphanumLess<std::string>()); 67 68 return true; 69 } 70 getHostLoggerEntries(const std::vector<std::filesystem::path> & hostLoggerFiles,uint64_t skip,uint64_t top,std::vector<std::string> & logEntries,size_t & logCount)71 inline bool getHostLoggerEntries( 72 const std::vector<std::filesystem::path>& hostLoggerFiles, uint64_t skip, 73 uint64_t top, std::vector<std::string>& logEntries, size_t& logCount) 74 { 75 GzFileReader logFile; 76 77 // Go though all log files and expose host logs. 78 for (const std::filesystem::path& it : hostLoggerFiles) 79 { 80 if (!logFile.gzGetLines(it.string(), skip, top, logEntries, logCount)) 81 { 82 BMCWEB_LOG_ERROR("fail to expose host logs"); 83 return false; 84 } 85 } 86 // Get lastMessage from constructor by getter 87 std::string lastMessage = logFile.getLastMessage(); 88 if (!lastMessage.empty()) 89 { 90 logCount++; 91 if (logCount > skip && logCount <= (skip + top)) 92 { 93 logEntries.push_back(lastMessage); 94 } 95 } 96 return true; 97 } 98 fillHostLoggerEntryJson(std::string_view logEntryID,std::string_view msg,nlohmann::json::object_t & logEntryJson)99 inline void fillHostLoggerEntryJson(std::string_view logEntryID, 100 std::string_view msg, 101 nlohmann::json::object_t& logEntryJson) 102 { 103 // Fill in the log entry with the gathered data. 104 logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 105 logEntryJson["@odata.id"] = boost::urls::format( 106 "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries/{}", 107 BMCWEB_REDFISH_SYSTEM_URI_NAME, logEntryID); 108 logEntryJson["Name"] = "Host Logger Entry"; 109 logEntryJson["Id"] = logEntryID; 110 logEntryJson["Message"] = msg; 111 logEntryJson["EntryType"] = log_entry::LogEntryType::Oem; 112 logEntryJson["Severity"] = log_entry::EventSeverity::OK; 113 logEntryJson["OemRecordFormat"] = "Host Logger Entry"; 114 } 115 handleSystemsLogServicesHostloggerGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName)116 inline void handleSystemsLogServicesHostloggerGet( 117 App& app, const crow::Request& req, 118 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 119 const std::string& systemName) 120 { 121 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 122 { 123 return; 124 } 125 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 126 { 127 // Option currently returns no systems. TBD 128 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 129 systemName); 130 return; 131 } 132 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 133 { 134 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 135 systemName); 136 return; 137 } 138 asyncResp->res.jsonValue["@odata.id"] = 139 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger", 140 BMCWEB_REDFISH_SYSTEM_URI_NAME); 141 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 142 asyncResp->res.jsonValue["Name"] = "Host Logger Service"; 143 asyncResp->res.jsonValue["Description"] = "Host Logger Service"; 144 asyncResp->res.jsonValue["Id"] = "HostLogger"; 145 asyncResp->res.jsonValue["Entries"]["@odata.id"] = 146 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 147 BMCWEB_REDFISH_SYSTEM_URI_NAME); 148 } 149 handleSystemsLogServicesHostloggerEntriesGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName)150 inline void handleSystemsLogServicesHostloggerEntriesGet( 151 App& app, const crow::Request& req, 152 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 153 const std::string& systemName) 154 { 155 query_param::QueryCapabilities capabilities = { 156 .canDelegateTop = true, 157 .canDelegateSkip = true, 158 }; 159 query_param::Query delegatedQuery; 160 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 161 delegatedQuery, capabilities)) 162 { 163 return; 164 } 165 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 166 { 167 // Option currently returns no systems. TBD 168 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 169 systemName); 170 return; 171 } 172 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 173 { 174 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 175 systemName); 176 return; 177 } 178 asyncResp->res.jsonValue["@odata.id"] = 179 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 180 BMCWEB_REDFISH_SYSTEM_URI_NAME); 181 asyncResp->res.jsonValue["@odata.type"] = 182 "#LogEntryCollection.LogEntryCollection"; 183 asyncResp->res.jsonValue["Name"] = "HostLogger Entries"; 184 asyncResp->res.jsonValue["Description"] = 185 "Collection of HostLogger Entries"; 186 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 187 logEntryArray = nlohmann::json::array(); 188 asyncResp->res.jsonValue["Members@odata.count"] = 0; 189 190 std::vector<std::filesystem::path> hostLoggerFiles; 191 if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 192 { 193 BMCWEB_LOG_DEBUG("Failed to get host log file path"); 194 return; 195 } 196 // If we weren't provided top and skip limits, use the defaults. 197 size_t skip = delegatedQuery.skip.value_or(0); 198 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 199 size_t logCount = 0; 200 // This vector only store the entries we want to expose that 201 // control by skip and top. 202 std::vector<std::string> logEntries; 203 if (!getHostLoggerEntries(hostLoggerFiles, skip, top, logEntries, logCount)) 204 { 205 messages::internalError(asyncResp->res); 206 return; 207 } 208 // If vector is empty, that means skip value larger than total 209 // log count 210 if (logEntries.empty()) 211 { 212 asyncResp->res.jsonValue["Members@odata.count"] = logCount; 213 return; 214 } 215 if (!logEntries.empty()) 216 { 217 for (size_t i = 0; i < logEntries.size(); i++) 218 { 219 nlohmann::json::object_t hostLogEntry; 220 fillHostLoggerEntryJson(std::to_string(skip + i), logEntries[i], 221 hostLogEntry); 222 logEntryArray.emplace_back(std::move(hostLogEntry)); 223 } 224 225 asyncResp->res.jsonValue["Members@odata.count"] = logCount; 226 if (skip + top < logCount) 227 { 228 asyncResp->res.jsonValue["Members@odata.nextLink"] = 229 std::format( 230 "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries?$skip=", 231 BMCWEB_REDFISH_SYSTEM_URI_NAME) + 232 std::to_string(skip + top); 233 } 234 } 235 } 236 handleSystemsLogServicesHostloggerEntriesEntryGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName,const std::string & param)237 inline void handleSystemsLogServicesHostloggerEntriesEntryGet( 238 App& app, const crow::Request& req, 239 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 240 const std::string& systemName, const std::string& param) 241 { 242 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 243 { 244 return; 245 } 246 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 247 { 248 // Option currently returns no systems. TBD 249 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 250 systemName); 251 return; 252 } 253 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 254 { 255 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 256 systemName); 257 return; 258 } 259 std::string_view targetID = param; 260 261 uint64_t idInt = 0; 262 263 auto [ptr, ec] = std::from_chars(targetID.begin(), targetID.end(), idInt); 264 if (ec != std::errc{} || ptr != targetID.end()) 265 { 266 messages::resourceNotFound(asyncResp->res, "LogEntry", param); 267 return; 268 } 269 270 std::vector<std::filesystem::path> hostLoggerFiles; 271 if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 272 { 273 BMCWEB_LOG_DEBUG("Failed to get host log file path"); 274 return; 275 } 276 277 size_t logCount = 0; 278 size_t top = 1; 279 std::vector<std::string> logEntries; 280 // We can get specific entry by skip and top. For example, if we 281 // want to get nth entry, we can set skip = n-1 and top = 1 to 282 // get that entry 283 if (!getHostLoggerEntries(hostLoggerFiles, idInt, top, logEntries, 284 logCount)) 285 { 286 messages::internalError(asyncResp->res); 287 return; 288 } 289 290 if (!logEntries.empty()) 291 { 292 nlohmann::json::object_t hostLogEntry; 293 fillHostLoggerEntryJson(targetID, logEntries[0], hostLogEntry); 294 asyncResp->res.jsonValue.update(hostLogEntry); 295 return; 296 } 297 298 // Requested ID was not found 299 messages::resourceNotFound(asyncResp->res, "LogEntry", param); 300 } 301 requestRoutesSystemsLogServiceHostlogger(App & app)302 inline void requestRoutesSystemsLogServiceHostlogger(App& app) 303 { 304 BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/") 305 .privileges(redfish::privileges::getLogService) 306 .methods(boost::beast::http::verb::get)(std::bind_front( 307 handleSystemsLogServicesHostloggerGet, std::ref(app))); 308 BMCWEB_ROUTE(app, 309 "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/") 310 .privileges(redfish::privileges::getLogEntryCollection) 311 .methods(boost::beast::http::verb::get)(std::bind_front( 312 handleSystemsLogServicesHostloggerEntriesGet, std::ref(app))); 313 314 BMCWEB_ROUTE( 315 app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/<str>/") 316 .privileges(redfish::privileges::getLogEntry) 317 .methods(boost::beast::http::verb::get)(std::bind_front( 318 handleSystemsLogServicesHostloggerEntriesEntryGet, std::ref(app))); 319 } 320 321 } // namespace redfish 322