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] =
76         std::from_chars(entryIDStrView.begin(), entryIDStrView.end(), index);
77     if (indexEc != std::errc() || ptr != entryIDStrView.end())
78     {
79         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
80         return false;
81     }
82     if (index <= 1)
83     {
84         // Indexes go directly from no postfix (handled above) to _2
85         // so if we ever see _0 or _1, it's incorrect
86         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
87         return false;
88     }
89 
90     // URI indexes are one based, journald is zero based
91     index -= 1;
92     return true;
93 }
94 
95 inline std::string getUniqueEntryID(uint64_t index, uint64_t curTs,
96                                     sd_id128_t& curBootID)
97 {
98     // make entryID as <bootID>_<timestamp>[_<index>]
99     std::array<char, SD_ID128_STRING_MAX> bootIDStr{};
100     sd_id128_to_string(curBootID, bootIDStr.data());
101     std::string postfix;
102     if (index > 0)
103     {
104         postfix = std::format("_{}", index + 1);
105     }
106     return std::format("{}_{}{}", bootIDStr.data(), curTs, postfix);
107 }
108 
109 inline int getJournalMetadata(sd_journal* journal, std::string_view field,
110                               std::string_view& contents)
111 {
112     const char* data = nullptr;
113     size_t length = 0;
114     int ret = 0;
115     // Get the metadata from the requested field of the journal entry
116     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
117     const void** dataVoid = reinterpret_cast<const void**>(&data);
118 
119     ret = sd_journal_get_data(journal, field.data(), dataVoid, &length);
120     if (ret < 0)
121     {
122         return ret;
123     }
124     contents = std::string_view(data, length);
125     // Only use the content after the "=" character.
126     contents.remove_prefix(std::min(contents.find('=') + 1, contents.size()));
127     return ret;
128 }
129 
130 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field,
131                                  const int& base, long int& contents)
132 {
133     int ret = 0;
134     std::string_view metadata;
135     // Get the metadata from the requested field of the journal entry
136     ret = getJournalMetadata(journal, field, metadata);
137     if (ret < 0)
138     {
139         return ret;
140     }
141     contents = strtol(metadata.data(), nullptr, base);
142     return ret;
143 }
144 
145 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp)
146 {
147     int ret = 0;
148     uint64_t timestamp = 0;
149     ret = sd_journal_get_realtime_usec(journal, &timestamp);
150     if (ret < 0)
151     {
152         BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
153         return false;
154     }
155     entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp);
156     return true;
157 }
158 
159 inline bool fillBMCJournalLogEntryJson(
160     const std::string& bmcJournalLogEntryID, sd_journal* journal,
161     nlohmann::json::object_t& bmcJournalLogEntryJson)
162 {
163     // Get the Log Entry contents
164     std::string message;
165     std::string_view syslogID;
166     int ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID);
167     if (ret < 0)
168     {
169         BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}",
170                          strerror(-ret));
171     }
172     if (!syslogID.empty())
173     {
174         message += std::string(syslogID) + ": ";
175     }
176 
177     std::string_view msg;
178     ret = getJournalMetadata(journal, "MESSAGE", msg);
179     if (ret < 0)
180     {
181         BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret));
182         return false;
183     }
184     message += std::string(msg);
185 
186     // Get the severity from the PRIORITY field
187     long int severity = 8; // Default to an invalid priority
188     ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity);
189     if (ret < 0)
190     {
191         BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret));
192     }
193 
194     // Get the Created time from the timestamp
195     std::string entryTimeStr;
196     if (!getEntryTimestamp(journal, entryTimeStr))
197     {
198         return false;
199     }
200 
201     // Fill in the log entry with the gathered data
202     bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
203     bmcJournalLogEntryJson["@odata.id"] = boost::urls::format(
204         "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}",
205         BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID);
206     bmcJournalLogEntryJson["Name"] = "BMC Journal Entry";
207     bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID;
208     bmcJournalLogEntryJson["Message"] = std::move(message);
209     bmcJournalLogEntryJson["EntryType"] = log_entry::LogEntryType::Oem;
210     log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK;
211     if (severity <= 2)
212     {
213         severityEnum = log_entry::EventSeverity::Critical;
214     }
215     else if (severity <= 4)
216     {
217         severityEnum = log_entry::EventSeverity::Warning;
218     }
219 
220     bmcJournalLogEntryJson["Severity"] = severityEnum;
221     bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry";
222     bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr);
223     return true;
224 }
225 
226 inline void handleManagersLogServiceJournalGet(
227     App& app, const crow::Request& req,
228     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
229     const std::string& managerId)
230 {
231     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
232     {
233         return;
234     }
235 
236     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
237     {
238         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
239         return;
240     }
241 
242     asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
243     asyncResp->res.jsonValue["@odata.id"] =
244         boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
245                             BMCWEB_REDFISH_MANAGER_URI_NAME);
246     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
247     asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
248     asyncResp->res.jsonValue["Id"] = "Journal";
249     asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
250 
251     std::pair<std::string, std::string> redfishDateTimeOffset =
252         redfish::time_utils::getDateTimeOffsetNow();
253     asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
254     asyncResp->res.jsonValue["DateTimeLocalOffset"] =
255         redfishDateTimeOffset.second;
256 
257     asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
258         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
259         BMCWEB_REDFISH_MANAGER_URI_NAME);
260 }
261 
262 struct JournalReadState
263 {
264     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal;
265     uint64_t index = 0;
266     sd_id128_t prevBootID{};
267     uint64_t prevTs = 0;
268 };
269 
270 inline void readJournalEntries(
271     uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
272     JournalReadState&& readState)
273 {
274     nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"];
275     nlohmann::json::array_t* logEntryArray =
276         logEntry.get_ptr<nlohmann::json::array_t*>();
277     if (logEntryArray == nullptr)
278     {
279         messages::internalError(asyncResp->res);
280         return;
281     }
282 
283     // The Journal APIs unfortunately do blocking calls to the filesystem, and
284     // can be somewhat expensive.  Short of creating our own io_uring based
285     // implementation of sd-journal, which would be difficult, the best thing we
286     // can do is to only parse a certain number of entries at a time.  The
287     // current chunk size is selected arbitrarily to ensure that we're not
288     // trying to process thousands of entries at the same time.
289     // The implementation will process the number of entries, then return
290     // control to the io_context to let other operations continue.
291     size_t segmentCountRemaining = 10;
292 
293     // Reset the unique ID on the first entry
294     for (uint64_t entryCount = logEntryArray->size();
295          entryCount < topEntryCount; entryCount++)
296     {
297         if (segmentCountRemaining == 0)
298         {
299             boost::asio::post(crow::connections::systemBus->get_io_context(),
300                               [asyncResp, topEntryCount,
301                                readState = std::move(readState)]() mutable {
302                                   readJournalEntries(topEntryCount, asyncResp,
303                                                      std::move(readState));
304                               });
305             return;
306         }
307 
308         // Get the entry timestamp
309         sd_id128_t curBootID{};
310         uint64_t curTs = 0;
311         int ret = sd_journal_get_monotonic_usec(readState.journal.get(), &curTs,
312                                                 &curBootID);
313         if (ret < 0)
314         {
315             BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
316                              strerror(-ret));
317             messages::internalError(asyncResp->res);
318             return;
319         }
320 
321         // If the timestamp isn't unique on the same boot, increment the index
322         bool sameBootIDs = sd_id128_equal(curBootID, readState.prevBootID) != 0;
323         if (sameBootIDs && (curTs == readState.prevTs))
324         {
325             readState.index++;
326         }
327         else
328         {
329             // Otherwise, reset it
330             readState.index = 0;
331         }
332 
333         // Save the bootID
334         readState.prevBootID = curBootID;
335 
336         // Save the timestamp
337         readState.prevTs = curTs;
338 
339         std::string idStr = getUniqueEntryID(readState.index, curTs, curBootID);
340 
341         nlohmann::json::object_t bmcJournalLogEntry;
342         if (!fillBMCJournalLogEntryJson(idStr, readState.journal.get(),
343                                         bmcJournalLogEntry))
344         {
345             messages::internalError(asyncResp->res);
346             return;
347         }
348         logEntryArray->emplace_back(std::move(bmcJournalLogEntry));
349 
350         ret = sd_journal_next(readState.journal.get());
351         if (ret < 0)
352         {
353             messages::internalError(asyncResp->res);
354             return;
355         }
356         if (ret == 0)
357         {
358             break;
359         }
360         segmentCountRemaining--;
361     }
362 }
363 
364 inline void handleManagersJournalLogEntryCollectionGet(
365     App& app, const crow::Request& req,
366     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
367     const std::string& managerId)
368 {
369     query_param::QueryCapabilities capabilities = {
370         .canDelegateTop = true,
371         .canDelegateSkip = true,
372     };
373     query_param::Query delegatedQuery;
374     if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
375                                                   delegatedQuery, capabilities))
376     {
377         return;
378     }
379 
380     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
381     {
382         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
383         return;
384     }
385 
386     size_t skip = delegatedQuery.skip.value_or(0);
387     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
388 
389     // Collections don't include the static data added by SubRoute
390     // because it has a duplicate entry for members
391     asyncResp->res.jsonValue["@odata.type"] =
392         "#LogEntryCollection.LogEntryCollection";
393     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
394         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
395         BMCWEB_REDFISH_MANAGER_URI_NAME);
396     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
397     asyncResp->res.jsonValue["Description"] =
398         "Collection of BMC Journal Entries";
399     asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t();
400 
401     // Go through the journal and use the timestamp to create a
402     // unique ID for each entry
403     sd_journal* journalTmp = nullptr;
404     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
405     if (ret < 0)
406     {
407         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
408         messages::internalError(asyncResp->res);
409         return;
410     }
411 
412     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
413         journalTmp, sd_journal_close);
414     journalTmp = nullptr;
415 
416     // Seek to the end
417     if (sd_journal_seek_tail(journal.get()) < 0)
418     {
419         messages::internalError(asyncResp->res);
420         return;
421     }
422 
423     // Get the last entry
424     if (sd_journal_previous(journal.get()) < 0)
425     {
426         messages::internalError(asyncResp->res);
427         return;
428     }
429 
430     // Get the last sequence number
431     uint64_t endSeqNum = 0;
432 #if SYSTEMD_VERSION >= 254
433     {
434         if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0)
435         {
436             messages::internalError(asyncResp->res);
437             return;
438         }
439     }
440 #endif
441 
442     // Seek to the beginning
443     if (sd_journal_seek_head(journal.get()) < 0)
444     {
445         messages::internalError(asyncResp->res);
446         return;
447     }
448 
449     // Get the first entry
450     if (sd_journal_next(journal.get()) < 0)
451     {
452         messages::internalError(asyncResp->res);
453         return;
454     }
455 
456     // Get the first sequence number
457     uint64_t startSeqNum = 0;
458 #if SYSTEMD_VERSION >= 254
459     {
460         if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0)
461         {
462             messages::internalError(asyncResp->res);
463             return;
464         }
465     }
466 #endif
467 
468     BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum,
469                      endSeqNum);
470 
471     // Add 1 to account for the last entry
472     uint64_t totalEntries = endSeqNum - startSeqNum + 1;
473     asyncResp->res.jsonValue["Members@odata.count"] = totalEntries;
474     if (skip + top < totalEntries)
475     {
476         asyncResp->res.jsonValue["Members@odata.nextLink"] =
477             boost::urls::format(
478                 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
479                 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
480     }
481     uint64_t index = 0;
482     sd_id128_t curBootID{};
483     uint64_t curTs = 0;
484     if (skip > 0)
485     {
486         if (sd_journal_next_skip(journal.get(), skip) < 0)
487         {
488             messages::internalError(asyncResp->res);
489             return;
490         }
491 
492         // Get the entry timestamp
493         ret = sd_journal_get_monotonic_usec(journal.get(), &curTs, &curBootID);
494         if (ret < 0)
495         {
496             BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
497                              strerror(-ret));
498             messages::internalError(asyncResp->res);
499             return;
500         }
501 
502         uint64_t endChunkSeqNum = 0;
503 #if SYSTEMD_VERSION >= 254
504         {
505             if (sd_journal_get_seqnum(journal.get(), &endChunkSeqNum, nullptr) <
506                 0)
507             {
508                 messages::internalError(asyncResp->res);
509                 return;
510             }
511         }
512 #endif
513 
514         // Seek to the first entry with the same timestamp and boot
515         ret = sd_journal_seek_monotonic_usec(journal.get(), curBootID, curTs);
516         if (ret < 0)
517         {
518             BMCWEB_LOG_ERROR("Failed to seek: {}", strerror(-ret));
519             messages::internalError(asyncResp->res);
520             return;
521         }
522         if (sd_journal_next(journal.get()) < 0)
523         {
524             messages::internalError(asyncResp->res);
525             return;
526         }
527         uint64_t startChunkSeqNum = 0;
528 #if SYSTEMD_VERSION >= 254
529         {
530             if (sd_journal_get_seqnum(journal.get(), &startChunkSeqNum,
531                                       nullptr) < 0)
532             {
533                 messages::internalError(asyncResp->res);
534                 return;
535             }
536         }
537 #endif
538 
539         // Get the difference between the start and end.  Most of the time this
540         // will be 0
541         BMCWEB_LOG_DEBUG("start={} end={}", startChunkSeqNum, endChunkSeqNum);
542         index = endChunkSeqNum - startChunkSeqNum;
543         if (index > endChunkSeqNum)
544         {
545             // Detect underflows.  Should never happen.
546             messages::internalError(asyncResp->res);
547             return;
548         }
549         if (index > 0)
550         {
551             BMCWEB_LOG_DEBUG("index = {}", index);
552             if (sd_journal_next_skip(journal.get(), index) < 0)
553             {
554                 messages::internalError(asyncResp->res);
555                 return;
556             }
557         }
558     }
559     // If this is the first entry of this series, reset the timestamps so the
560     // Index doesn't increment
561     if (index == 0)
562     {
563         curBootID = {};
564         curTs = 0;
565     }
566     else
567     {
568         index -= 1;
569     }
570     BMCWEB_LOG_DEBUG("Index was {}", index);
571     readJournalEntries(top, asyncResp,
572                        {std::move(journal), index, curBootID, curTs});
573 }
574 
575 inline void handleManagersJournalEntriesLogEntryGet(
576     App& app, const crow::Request& req,
577     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
578     const std::string& managerId, const std::string& entryID)
579 {
580     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
581     {
582         return;
583     }
584 
585     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
586     {
587         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
588         return;
589     }
590 
591     // Convert the unique ID back to a timestamp to find the entry
592     sd_id128_t bootID{};
593     uint64_t ts = 0;
594     uint64_t index = 0;
595     if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index))
596     {
597         return;
598     }
599 
600     sd_journal* journalTmp = nullptr;
601     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
602     if (ret < 0)
603     {
604         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
605         messages::internalError(asyncResp->res);
606         return;
607     }
608     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
609         journalTmp, sd_journal_close);
610     journalTmp = nullptr;
611     // Go to the timestamp in the log and move to the entry at the
612     // index tracking the unique ID
613     ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts);
614     if (ret < 0)
615     {
616         BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}",
617                          strerror(-ret));
618         messages::internalError(asyncResp->res);
619         return;
620     }
621 
622     if (sd_journal_next_skip(journal.get(), index + 1) < 0)
623     {
624         messages::internalError(asyncResp->res);
625         return;
626     }
627 
628     nlohmann::json::object_t bmcJournalLogEntry;
629     if (!fillBMCJournalLogEntryJson(entryID, journal.get(), bmcJournalLogEntry))
630     {
631         messages::internalError(asyncResp->res);
632         return;
633     }
634     asyncResp->res.jsonValue.update(bmcJournalLogEntry);
635 }
636 
637 inline void requestRoutesBMCJournalLogService(App& app)
638 {
639     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
640         .privileges(redfish::privileges::getLogService)
641         .methods(boost::beast::http::verb::get)(
642             std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
643 
644     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
645         .privileges(redfish::privileges::getLogEntryCollection)
646         .methods(boost::beast::http::verb::get)(std::bind_front(
647             handleManagersJournalLogEntryCollectionGet, std::ref(app)));
648 
649     BMCWEB_ROUTE(
650         app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
651         .privileges(redfish::privileges::getLogEntry)
652         .methods(boost::beast::http::verb::get)(std::bind_front(
653             handleManagersJournalEntriesLogEntryGet, std::ref(app)));
654 }
655 } // namespace redfish
656