1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4
5 #include "app.hpp"
6 #include "error_messages.hpp"
7 #include "generated/enums/log_entry.hpp"
8 #include "query.hpp"
9 #include "registries/base_message_registry.hpp"
10 #include "registries/privilege_registry.hpp"
11 #include "utils/journal_utils.hpp"
12 #include "utils/time_utils.hpp"
13
14 #include <systemd/sd-journal.h>
15
16 #include <boost/beast/http/verb.hpp>
17
18 #include <array>
19 #include <memory>
20 #include <string>
21 #include <string_view>
22
23 namespace redfish
24 {
25
handleManagersLogServiceJournalGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)26 inline void handleManagersLogServiceJournalGet(
27 App& app, const crow::Request& req,
28 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
29 const std::string& managerId)
30 {
31 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
32 {
33 return;
34 }
35
36 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
37 {
38 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
39 return;
40 }
41
42 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
43 asyncResp->res.jsonValue["@odata.id"] =
44 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
45 BMCWEB_REDFISH_MANAGER_URI_NAME);
46 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
47 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
48 asyncResp->res.jsonValue["Id"] = "Journal";
49 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
50
51 std::pair<std::string, std::string> redfishDateTimeOffset =
52 redfish::time_utils::getDateTimeOffsetNow();
53 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
54 asyncResp->res.jsonValue["DateTimeLocalOffset"] =
55 redfishDateTimeOffset.second;
56
57 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
58 "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
59 BMCWEB_REDFISH_MANAGER_URI_NAME);
60 }
61
62 struct JournalReadState
63 {
64 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal;
65 };
66
readJournalEntries(uint64_t topEntryCount,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,JournalReadState && readState)67 inline void readJournalEntries(
68 uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
69 JournalReadState&& readState)
70 {
71 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"];
72 nlohmann::json::array_t* logEntryArray =
73 logEntry.get_ptr<nlohmann::json::array_t*>();
74 if (logEntryArray == nullptr)
75 {
76 messages::internalError(asyncResp->res);
77 return;
78 }
79
80 // The Journal APIs unfortunately do blocking calls to the filesystem, and
81 // can be somewhat expensive. Short of creating our own io_uring based
82 // implementation of sd-journal, which would be difficult, the best thing we
83 // can do is to only parse a certain number of entries at a time. The
84 // current chunk size is selected arbitrarily to ensure that we're not
85 // trying to process thousands of entries at the same time.
86 // The implementation will process the number of entries, then return
87 // control to the io_context to let other operations continue.
88 size_t segmentCountRemaining = 10;
89
90 // Reset the unique ID on the first entry
91 for (uint64_t entryCount = logEntryArray->size();
92 entryCount < topEntryCount; entryCount++)
93 {
94 if (segmentCountRemaining == 0)
95 {
96 boost::asio::post(crow::connections::systemBus->get_io_context(),
97 [asyncResp, topEntryCount,
98 readState = std::move(readState)]() mutable {
99 readJournalEntries(topEntryCount, asyncResp,
100 std::move(readState));
101 });
102 return;
103 }
104
105 nlohmann::json::object_t bmcJournalLogEntry;
106 if (!fillBMCJournalLogEntryJson(readState.journal.get(),
107 bmcJournalLogEntry))
108 {
109 messages::internalError(asyncResp->res);
110 return;
111 }
112 logEntryArray->emplace_back(std::move(bmcJournalLogEntry));
113
114 int ret = sd_journal_next(readState.journal.get());
115 if (ret < 0)
116 {
117 messages::internalError(asyncResp->res);
118 return;
119 }
120 if (ret == 0)
121 {
122 break;
123 }
124 segmentCountRemaining--;
125 }
126 }
127
handleManagersJournalLogEntryCollectionGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)128 inline void handleManagersJournalLogEntryCollectionGet(
129 App& app, const crow::Request& req,
130 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
131 const std::string& managerId)
132 {
133 query_param::QueryCapabilities capabilities = {
134 .canDelegateTop = true,
135 .canDelegateSkip = true,
136 };
137 query_param::Query delegatedQuery;
138 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
139 delegatedQuery, capabilities))
140 {
141 return;
142 }
143
144 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
145 {
146 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
147 return;
148 }
149
150 size_t skip = delegatedQuery.skip.value_or(0);
151 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
152
153 // Collections don't include the static data added by SubRoute
154 // because it has a duplicate entry for members
155 asyncResp->res.jsonValue["@odata.type"] =
156 "#LogEntryCollection.LogEntryCollection";
157 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
158 "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
159 BMCWEB_REDFISH_MANAGER_URI_NAME);
160 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
161 asyncResp->res.jsonValue["Description"] =
162 "Collection of BMC Journal Entries";
163 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t();
164
165 // Go through the journal and use the timestamp to create a
166 // unique ID for each entry
167 sd_journal* journalTmp = nullptr;
168 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
169 if (ret < 0)
170 {
171 BMCWEB_LOG_ERROR("failed to open journal: {}", ret);
172 messages::internalError(asyncResp->res);
173 return;
174 }
175
176 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
177 journalTmp, sd_journal_close);
178 journalTmp = nullptr;
179
180 // Seek to the end
181 if (sd_journal_seek_tail(journal.get()) < 0)
182 {
183 messages::internalError(asyncResp->res);
184 return;
185 }
186
187 // Get the last entry
188 if (sd_journal_previous(journal.get()) < 0)
189 {
190 messages::internalError(asyncResp->res);
191 return;
192 }
193
194 // Get the last sequence number
195 uint64_t endSeqNum = 0;
196 #if LIBSYSTEMD_VERSION >= 254
197 {
198 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0)
199 {
200 messages::internalError(asyncResp->res);
201 return;
202 }
203 }
204 #endif
205
206 // Seek to the beginning
207 if (sd_journal_seek_head(journal.get()) < 0)
208 {
209 messages::internalError(asyncResp->res);
210 return;
211 }
212
213 // Get the first entry
214 if (sd_journal_next(journal.get()) < 0)
215 {
216 messages::internalError(asyncResp->res);
217 return;
218 }
219
220 // Get the first sequence number
221 uint64_t startSeqNum = 0;
222 #if LIBSYSTEMD_VERSION >= 254
223 {
224 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0)
225 {
226 messages::internalError(asyncResp->res);
227 return;
228 }
229 }
230 #endif
231
232 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum,
233 endSeqNum);
234
235 // Add 1 to account for the last entry
236 uint64_t totalEntries = endSeqNum - startSeqNum + 1;
237 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries;
238 if (skip + top < totalEntries)
239 {
240 asyncResp->res.jsonValue["Members@odata.nextLink"] =
241 boost::urls::format(
242 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
243 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
244 }
245 uint64_t index = 0;
246 if (skip > 0)
247 {
248 if (sd_journal_next_skip(journal.get(), skip) < 0)
249 {
250 messages::internalError(asyncResp->res);
251 return;
252 }
253 }
254 BMCWEB_LOG_DEBUG("Index was {}", index);
255 readJournalEntries(top, asyncResp, {std::move(journal)});
256 }
257
handleManagersJournalEntriesLogEntryGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId,const std::string & entryID)258 inline void handleManagersJournalEntriesLogEntryGet(
259 App& app, const crow::Request& req,
260 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
261 const std::string& managerId, const std::string& entryID)
262 {
263 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
264 {
265 return;
266 }
267
268 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
269 {
270 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
271 return;
272 }
273
274 sd_journal* journalTmp = nullptr;
275 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
276 if (ret < 0)
277 {
278 BMCWEB_LOG_ERROR("failed to open journal: {}", ret);
279 messages::internalError(asyncResp->res);
280 return;
281 }
282 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
283 journalTmp, sd_journal_close);
284 journalTmp = nullptr;
285
286 std::string cursor;
287 if (!crow::utility::base64Decode(entryID, cursor))
288 {
289 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
290 return;
291 }
292
293 // Go to the cursor in the log
294 ret = sd_journal_seek_cursor(journal.get(), cursor.c_str());
295 if (ret < 0)
296 {
297 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
298 return;
299 }
300
301 if (sd_journal_next(journal.get()) < 0)
302 {
303 messages::internalError(asyncResp->res);
304 return;
305 }
306
307 ret = sd_journal_test_cursor(journal.get(), cursor.c_str());
308 if (ret == 0)
309 {
310 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
311 return;
312 }
313 if (ret < 0)
314 {
315 messages::internalError(asyncResp->res);
316 return;
317 }
318
319 nlohmann::json::object_t bmcJournalLogEntry;
320 if (!fillBMCJournalLogEntryJson(journal.get(), bmcJournalLogEntry))
321 {
322 messages::internalError(asyncResp->res);
323 return;
324 }
325 asyncResp->res.jsonValue.update(bmcJournalLogEntry);
326 }
327
requestRoutesBMCJournalLogService(App & app)328 inline void requestRoutesBMCJournalLogService(App& app)
329 {
330 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
331 .privileges(redfish::privileges::getLogService)
332 .methods(boost::beast::http::verb::get)(
333 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
334
335 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
336 .privileges(redfish::privileges::getLogEntryCollection)
337 .methods(boost::beast::http::verb::get)(std::bind_front(
338 handleManagersJournalLogEntryCollectionGet, std::ref(app)));
339
340 BMCWEB_ROUTE(
341 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
342 .privileges(redfish::privileges::getLogEntry)
343 .methods(boost::beast::http::verb::get)(std::bind_front(
344 handleManagersJournalEntriesLogEntryGet, std::ref(app)));
345 }
346 } // namespace redfish
347