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/time_utils.hpp"
10 
11 #include <systemd/sd-journal.h>
12 
13 #include <boost/beast/http/verb.hpp>
14 
15 #include <array>
16 #include <memory>
17 #include <string>
18 #include <string_view>
19 
20 namespace redfish
21 {
22 // Entry is formed like "BootID_timestamp" or "BootID_timestamp_index"
23 inline bool
24     getTimestampFromID(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
25                        std::string_view entryIDStrView, sd_id128_t& bootID,
26                        uint64_t& timestamp, uint64_t& index)
27 {
28     // Convert the unique ID back to a bootID + timestamp to find the entry
29     auto underscore1Pos = entryIDStrView.find('_');
30     if (underscore1Pos == std::string_view::npos)
31     {
32         // EntryID has no bootID or timestamp
33         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
34         return false;
35     }
36 
37     // EntryID has bootID + timestamp
38 
39     // Convert entryIDViewString to BootID
40     // NOTE: bootID string which needs to be null-terminated for
41     // sd_id128_from_string()
42     std::string bootIDStr(entryIDStrView.substr(0, underscore1Pos));
43     if (sd_id128_from_string(bootIDStr.c_str(), &bootID) < 0)
44     {
45         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
46         return false;
47     }
48 
49     // Get the timestamp from entryID
50     entryIDStrView.remove_prefix(underscore1Pos + 1);
51 
52     auto [timestampEnd, tstampEc] = std::from_chars(
53         entryIDStrView.begin(), entryIDStrView.end(), timestamp);
54     if (tstampEc != std::errc())
55     {
56         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
57         return false;
58     }
59     entryIDStrView = std::string_view(
60         timestampEnd,
61         static_cast<size_t>(std::distance(timestampEnd, entryIDStrView.end())));
62     if (entryIDStrView.empty())
63     {
64         index = 0U;
65         return true;
66     }
67     // Timestamp might include optional index, if two events happened at the
68     // same "time".
69     if (entryIDStrView[0] != '_')
70     {
71         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
72         return false;
73     }
74     entryIDStrView.remove_prefix(1);
75     auto [ptr, indexEc] = std::from_chars(entryIDStrView.begin(),
76                                           entryIDStrView.end(), index);
77     if (indexEc != std::errc() || ptr != entryIDStrView.end())
78     {
79         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
80         return false;
81     }
82     return true;
83 }
84 
85 inline bool getUniqueEntryID(sd_journal* journal, std::string& entryID,
86                              const bool firstEntry = true)
87 {
88     int ret = 0;
89     static sd_id128_t prevBootID{};
90     static uint64_t prevTs = 0;
91     static int index = 0;
92     if (firstEntry)
93     {
94         prevBootID = {};
95         prevTs = 0;
96     }
97 
98     // Get the entry timestamp
99     uint64_t curTs = 0;
100     sd_id128_t curBootID{};
101     ret = sd_journal_get_monotonic_usec(journal, &curTs, &curBootID);
102     if (ret < 0)
103     {
104         BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
105         return false;
106     }
107     // If the timestamp isn't unique on the same boot, increment the index
108     bool sameBootIDs = sd_id128_equal(curBootID, prevBootID) != 0;
109     if (sameBootIDs && (curTs == prevTs))
110     {
111         index++;
112     }
113     else
114     {
115         // Otherwise, reset it
116         index = 0;
117     }
118 
119     if (!sameBootIDs)
120     {
121         // Save the bootID
122         prevBootID = curBootID;
123     }
124     // Save the timestamp
125     prevTs = curTs;
126 
127     // make entryID as <bootID>_<timestamp>[_<index>]
128     std::array<char, SD_ID128_STRING_MAX> bootIDStr{};
129     sd_id128_to_string(curBootID, bootIDStr.data());
130     entryID = std::format("{}_{}", bootIDStr.data(), curTs);
131     if (index > 0)
132     {
133         entryID += "_" + std::to_string(index);
134     }
135     return true;
136 }
137 
138 inline int getJournalMetadata(sd_journal* journal, std::string_view field,
139                               std::string_view& contents)
140 {
141     const char* data = nullptr;
142     size_t length = 0;
143     int ret = 0;
144     // Get the metadata from the requested field of the journal entry
145     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
146     const void** dataVoid = reinterpret_cast<const void**>(&data);
147 
148     ret = sd_journal_get_data(journal, field.data(), dataVoid, &length);
149     if (ret < 0)
150     {
151         return ret;
152     }
153     contents = std::string_view(data, length);
154     // Only use the content after the "=" character.
155     contents.remove_prefix(std::min(contents.find('=') + 1, contents.size()));
156     return ret;
157 }
158 
159 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field,
160                                  const int& base, long int& contents)
161 {
162     int ret = 0;
163     std::string_view metadata;
164     // Get the metadata from the requested field of the journal entry
165     ret = getJournalMetadata(journal, field, metadata);
166     if (ret < 0)
167     {
168         return ret;
169     }
170     contents = strtol(metadata.data(), nullptr, base);
171     return ret;
172 }
173 
174 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp)
175 {
176     int ret = 0;
177     uint64_t timestamp = 0;
178     ret = sd_journal_get_realtime_usec(journal, &timestamp);
179     if (ret < 0)
180     {
181         BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
182         return false;
183     }
184     entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp);
185     return true;
186 }
187 
188 inline int
189     fillBMCJournalLogEntryJson(const std::string& bmcJournalLogEntryID,
190                                sd_journal* journal,
191                                nlohmann::json::object_t& bmcJournalLogEntryJson)
192 {
193     // Get the Log Entry contents
194     int ret = 0;
195 
196     std::string message;
197     std::string_view syslogID;
198     ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID);
199     if (ret < 0)
200     {
201         BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}",
202                          strerror(-ret));
203     }
204     if (!syslogID.empty())
205     {
206         message += std::string(syslogID) + ": ";
207     }
208 
209     std::string_view msg;
210     ret = getJournalMetadata(journal, "MESSAGE", msg);
211     if (ret < 0)
212     {
213         BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret));
214         return 1;
215     }
216     message += std::string(msg);
217 
218     // Get the severity from the PRIORITY field
219     long int severity = 8; // Default to an invalid priority
220     ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity);
221     if (ret < 0)
222     {
223         BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret));
224     }
225 
226     // Get the Created time from the timestamp
227     std::string entryTimeStr;
228     if (!getEntryTimestamp(journal, entryTimeStr))
229     {
230         return 1;
231     }
232 
233     // Fill in the log entry with the gathered data
234     bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
235     bmcJournalLogEntryJson["@odata.id"] = boost::urls::format(
236         "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}",
237         BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID);
238     bmcJournalLogEntryJson["Name"] = "BMC Journal Entry";
239     bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID;
240     bmcJournalLogEntryJson["Message"] = std::move(message);
241     bmcJournalLogEntryJson["EntryType"] = "Oem";
242     log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK;
243     if (severity <= 2)
244     {
245         severityEnum = log_entry::EventSeverity::Critical;
246     }
247     else if (severity <= 4)
248     {
249         severityEnum = log_entry::EventSeverity::Warning;
250     }
251 
252     bmcJournalLogEntryJson["Severity"] = severityEnum;
253     bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry";
254     bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr);
255     return 0;
256 }
257 
258 inline void handleManagersLogServiceJournalGet(
259     App& app, const crow::Request& req,
260     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
261     const std::string& managerId)
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     asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
275     asyncResp->res.jsonValue["@odata.id"] =
276         boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
277                             BMCWEB_REDFISH_MANAGER_URI_NAME);
278     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
279     asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
280     asyncResp->res.jsonValue["Id"] = "Journal";
281     asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
282 
283     std::pair<std::string, std::string> redfishDateTimeOffset =
284         redfish::time_utils::getDateTimeOffsetNow();
285     asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
286     asyncResp->res.jsonValue["DateTimeLocalOffset"] =
287         redfishDateTimeOffset.second;
288 
289     asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
290         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
291         BMCWEB_REDFISH_MANAGER_URI_NAME);
292 }
293 
294 inline void handleManagersJournalLogEntryCollectionGet(
295     App& app, const crow::Request& req,
296     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
297     const std::string& managerId)
298 {
299     query_param::QueryCapabilities capabilities = {
300         .canDelegateTop = true,
301         .canDelegateSkip = true,
302     };
303     query_param::Query delegatedQuery;
304     if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
305                                                   delegatedQuery, capabilities))
306     {
307         return;
308     }
309 
310     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
311     {
312         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
313         return;
314     }
315 
316     size_t skip = delegatedQuery.skip.value_or(0);
317     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
318 
319     // Collections don't include the static data added by SubRoute
320     // because it has a duplicate entry for members
321     asyncResp->res.jsonValue["@odata.type"] =
322         "#LogEntryCollection.LogEntryCollection";
323     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
324         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
325         BMCWEB_REDFISH_MANAGER_URI_NAME);
326     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
327     asyncResp->res.jsonValue["Description"] =
328         "Collection of BMC Journal Entries";
329     nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
330     logEntryArray = nlohmann::json::array();
331 
332     // Go through the journal and use the timestamp to create a
333     // unique ID for each entry
334     sd_journal* journalTmp = nullptr;
335     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
336     if (ret < 0)
337     {
338         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
339         messages::internalError(asyncResp->res);
340         return;
341     }
342     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
343         journalTmp, sd_journal_close);
344     journalTmp = nullptr;
345     uint64_t entryCount = 0;
346     // Reset the unique ID on the first entry
347     bool firstEntry = true;
348     SD_JOURNAL_FOREACH(journal.get())
349     {
350         entryCount++;
351         // Handle paging using skip (number of entries to skip from
352         // the start) and top (number of entries to display)
353         if (entryCount <= skip || entryCount > skip + top)
354         {
355             continue;
356         }
357 
358         std::string idStr;
359         if (!getUniqueEntryID(journal.get(), idStr, firstEntry))
360         {
361             continue;
362         }
363         firstEntry = false;
364 
365         nlohmann::json::object_t bmcJournalLogEntry;
366         if (fillBMCJournalLogEntryJson(idStr, journal.get(),
367                                        bmcJournalLogEntry) != 0)
368         {
369             messages::internalError(asyncResp->res);
370             return;
371         }
372         logEntryArray.emplace_back(std::move(bmcJournalLogEntry));
373     }
374     asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
375     if (skip + top < entryCount)
376     {
377         asyncResp->res.jsonValue["Members@odata.nextLink"] =
378             boost::urls::format(
379                 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
380                 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
381     }
382 }
383 
384 inline void handleManagersJournalEntriesLogEntryGet(
385     App& app, const crow::Request& req,
386     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
387     const std::string& managerId, const std::string& entryID)
388 {
389     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
390     {
391         return;
392     }
393 
394     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
395     {
396         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
397         return;
398     }
399 
400     // Convert the unique ID back to a timestamp to find the entry
401     sd_id128_t bootID{};
402     uint64_t ts = 0;
403     uint64_t index = 0;
404     if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index))
405     {
406         return;
407     }
408 
409     sd_journal* journalTmp = nullptr;
410     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
411     if (ret < 0)
412     {
413         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
414         messages::internalError(asyncResp->res);
415         return;
416     }
417     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
418         journalTmp, sd_journal_close);
419     journalTmp = nullptr;
420     // Go to the timestamp in the log and move to the entry at the
421     // index tracking the unique ID
422     std::string idStr;
423     bool firstEntry = true;
424     ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts);
425     if (ret < 0)
426     {
427         BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}",
428                          strerror(-ret));
429         messages::internalError(asyncResp->res);
430         return;
431     }
432     for (uint64_t i = 0; i <= index; i++)
433     {
434         sd_journal_next(journal.get());
435         if (!getUniqueEntryID(journal.get(), idStr, firstEntry))
436         {
437             messages::internalError(asyncResp->res);
438             return;
439         }
440         firstEntry = false;
441     }
442     // Confirm that the entry ID matches what was requested
443     if (idStr != entryID)
444     {
445         messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
446         return;
447     }
448 
449     nlohmann::json::object_t bmcJournalLogEntry;
450     if (fillBMCJournalLogEntryJson(entryID, journal.get(),
451                                    bmcJournalLogEntry) != 0)
452     {
453         messages::internalError(asyncResp->res);
454         return;
455     }
456     asyncResp->res.jsonValue.update(bmcJournalLogEntry);
457 };
458 
459 inline void requestRoutesBMCJournalLogService(App& app)
460 {
461     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
462         .privileges(redfish::privileges::getLogService)
463         .methods(boost::beast::http::verb::get)(
464             std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
465 
466     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
467         .privileges(redfish::privileges::getLogEntryCollection)
468         .methods(boost::beast::http::verb::get)(std::bind_front(
469             handleManagersJournalLogEntryCollectionGet, std::ref(app)));
470 
471     BMCWEB_ROUTE(
472         app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
473         .privileges(redfish::privileges::getLogEntry)
474         .methods(boost::beast::http::verb::get)(std::bind_front(
475             handleManagersJournalEntriesLogEntryGet, std::ref(app)));
476 }
477 } // namespace redfish
478