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