xref: /openbmc/bmcweb/redfish-core/lib/manager_logservices_journal.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
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