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