1 /* 2 // Copyright (c) 2020 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 #pragma once 17 #include "node.hpp" 18 #include "registries.hpp" 19 #include "registries/base_message_registry.hpp" 20 #include "registries/openbmc_message_registry.hpp" 21 22 #include <sys/inotify.h> 23 24 #include <boost/container/flat_map.hpp> 25 #include <cstdlib> 26 #include <ctime> 27 #include <error_messages.hpp> 28 #include <http_client.hpp> 29 #include <memory> 30 #include <utils/json_utils.hpp> 31 #include <variant> 32 33 namespace redfish 34 { 35 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 36 constexpr const char* redfishEventLogFile = "/var/log/redfish"; 37 constexpr const uint32_t inotifyFileAction = IN_MODIFY; 38 std::shared_ptr<boost::asio::posix::stream_descriptor> inotifyConn = nullptr; 39 40 // <ID, timestamp, RedfishLogId, registryPrefix, MessageId, MessageArgs> 41 using EventLogObjectsType = 42 std::tuple<std::string, std::string, std::string, std::string, std::string, 43 boost::beast::span<std::string>>; 44 45 namespace message_registries 46 { 47 static const Message* 48 getMsgFromRegistry(const std::string& messageKey, 49 const boost::beast::span<const MessageEntry>& registry) 50 { 51 boost::beast::span<const MessageEntry>::const_iterator messageIt = 52 std::find_if(registry.cbegin(), registry.cend(), 53 [&messageKey](const MessageEntry& messageEntry) { 54 return !messageKey.compare(messageEntry.first); 55 }); 56 if (messageIt != registry.cend()) 57 { 58 return &messageIt->second; 59 } 60 61 return nullptr; 62 } 63 64 static const Message* formatMessage(const std::string_view& messageID) 65 { 66 // Redfish MessageIds are in the form 67 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 68 // the right Message 69 std::vector<std::string> fields; 70 fields.reserve(4); 71 boost::split(fields, messageID, boost::is_any_of(".")); 72 if (fields.size() != 4) 73 { 74 return nullptr; 75 } 76 std::string& registryName = fields[0]; 77 std::string& messageKey = fields[3]; 78 79 // Find the right registry and check it for the MessageKey 80 if (std::string(base::header.registryPrefix) == registryName) 81 { 82 return getMsgFromRegistry( 83 messageKey, boost::beast::span<const MessageEntry>(base::registry)); 84 } 85 if (std::string(openbmc::header.registryPrefix) == registryName) 86 { 87 return getMsgFromRegistry( 88 messageKey, 89 boost::beast::span<const MessageEntry>(openbmc::registry)); 90 } 91 return nullptr; 92 } 93 } // namespace message_registries 94 95 namespace event_log 96 { 97 bool getUniqueEntryID(const std::string& logEntry, std::string& entryID, 98 const bool firstEntry = true) 99 { 100 static time_t prevTs = 0; 101 static int index = 0; 102 if (firstEntry) 103 { 104 prevTs = 0; 105 } 106 107 // Get the entry timestamp 108 std::time_t curTs = 0; 109 std::tm timeStruct = {}; 110 std::istringstream entryStream(logEntry); 111 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 112 { 113 curTs = std::mktime(&timeStruct); 114 if (curTs == -1) 115 { 116 return false; 117 } 118 } 119 // If the timestamp isn't unique, increment the index 120 index = (curTs == prevTs) ? index + 1 : 0; 121 122 // Save the timestamp 123 prevTs = curTs; 124 125 entryID = std::to_string(curTs); 126 if (index > 0) 127 { 128 entryID += "_" + std::to_string(index); 129 } 130 return true; 131 } 132 133 int getEventLogParams(const std::string& logEntry, std::string& timestamp, 134 std::string& messageID, 135 boost::beast::span<std::string>& messageArgs) 136 { 137 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>" 138 // First get the Timestamp 139 size_t space = logEntry.find_first_of(" "); 140 if (space == std::string::npos) 141 { 142 return -EINVAL; 143 } 144 timestamp = logEntry.substr(0, space); 145 // Then get the log contents 146 size_t entryStart = logEntry.find_first_not_of(" ", space); 147 if (entryStart == std::string::npos) 148 { 149 return -EINVAL; 150 } 151 std::string_view entry(logEntry); 152 entry.remove_prefix(entryStart); 153 // Use split to separate the entry into its fields 154 std::vector<std::string> logEntryFields; 155 boost::split(logEntryFields, entry, boost::is_any_of(","), 156 boost::token_compress_on); 157 // We need at least a MessageId to be valid 158 if (logEntryFields.size() < 1) 159 { 160 return -EINVAL; 161 } 162 messageID = logEntryFields[0]; 163 164 // Get the MessageArgs from the log if there are any 165 if (logEntryFields.size() > 1) 166 { 167 std::string& messageArgsStart = logEntryFields[1]; 168 // If the first string is empty, assume there are no MessageArgs 169 std::size_t messageArgsSize = 0; 170 if (!messageArgsStart.empty()) 171 { 172 messageArgsSize = logEntryFields.size() - 1; 173 } 174 175 messageArgs = boost::beast::span(&messageArgsStart, messageArgsSize); 176 } 177 178 return 0; 179 } 180 181 void getRegistryAndMessageKey(const std::string& messageID, 182 std::string& registryName, 183 std::string& messageKey) 184 { 185 // Redfish MessageIds are in the form 186 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 187 // the right Message 188 std::vector<std::string> fields; 189 fields.reserve(4); 190 boost::split(fields, messageID, boost::is_any_of(".")); 191 if (fields.size() == 4) 192 { 193 registryName = fields[0]; 194 messageKey = fields[3]; 195 } 196 } 197 198 int formatEventLogEntry(const std::string& logEntryID, 199 const std::string& messageID, 200 const boost::beast::span<std::string>& messageArgs, 201 std::string timestamp, const std::string customText, 202 nlohmann::json& logEntryJson) 203 { 204 // Get the Message from the MessageRegistry 205 const message_registries::Message* message = 206 message_registries::formatMessage(messageID); 207 208 std::string msg; 209 std::string severity; 210 if (message != nullptr) 211 { 212 msg = message->message; 213 severity = message->severity; 214 } 215 216 // Fill the MessageArgs into the Message 217 int i = 0; 218 for (const std::string& messageArg : messageArgs) 219 { 220 std::string argStr = "%" + std::to_string(++i); 221 size_t argPos = msg.find(argStr); 222 if (argPos != std::string::npos) 223 { 224 msg.replace(argPos, argStr.length(), messageArg); 225 } 226 } 227 228 // Get the Created time from the timestamp. The log timestamp is in 229 // RFC3339 format which matches the Redfish format except for the 230 // fractional seconds between the '.' and the '+', so just remove them. 231 std::size_t dot = timestamp.find_first_of("."); 232 std::size_t plus = timestamp.find_first_of("+"); 233 if (dot != std::string::npos && plus != std::string::npos) 234 { 235 timestamp.erase(dot, plus - dot); 236 } 237 238 // Fill in the log entry with the gathered data 239 logEntryJson = {{"EventId", logEntryID}, 240 {"EventType", "Event"}, 241 {"Severity", std::move(severity)}, 242 {"Message", std::move(msg)}, 243 {"MessageId", std::move(messageID)}, 244 {"MessageArgs", std::move(messageArgs)}, 245 {"EventTimestamp", std::move(timestamp)}, 246 {"Context", customText}}; 247 return 0; 248 } 249 #endif 250 251 } // namespace event_log 252 253 class Subscription 254 { 255 public: 256 std::string id; 257 std::string destinationUrl; 258 std::string protocol; 259 std::string retryPolicy; 260 std::string customText; 261 std::string eventFormatType; 262 std::string subscriptionType; 263 std::vector<std::string> registryMsgIds; 264 std::vector<std::string> registryPrefixes; 265 std::vector<nlohmann::json> httpHeaders; // key-value pair 266 267 Subscription(const Subscription&) = delete; 268 Subscription& operator=(const Subscription&) = delete; 269 Subscription(Subscription&&) = delete; 270 Subscription& operator=(Subscription&&) = delete; 271 272 Subscription(const std::string& inHost, const std::string& inPort, 273 const std::string& inPath, const std::string& inUriProto) : 274 eventSeqNum(1), 275 host(inHost), port(inPort), path(inPath), uriProto(inUriProto) 276 { 277 conn = std::make_shared<crow::HttpClient>( 278 crow::connections::systemBus->get_io_context(), host, port, path); 279 } 280 ~Subscription() 281 { 282 } 283 284 void sendEvent(const std::string& msg) 285 { 286 std::vector<std::pair<std::string, std::string>> reqHeaders; 287 for (const auto& header : httpHeaders) 288 { 289 for (const auto& item : header.items()) 290 { 291 std::string key = item.key(); 292 std::string val = item.value(); 293 reqHeaders.emplace_back(std::pair(key, val)); 294 } 295 } 296 conn->setHeaders(reqHeaders); 297 conn->sendData(msg); 298 } 299 300 void sendTestEventLog() 301 { 302 nlohmann::json logEntryArray; 303 logEntryArray.push_back({}); 304 nlohmann::json& logEntryJson = logEntryArray.back(); 305 306 logEntryJson = {{"EventId", "TestID"}, 307 {"EventType", "Event"}, 308 {"Severity", "OK"}, 309 {"Message", "Generated test event"}, 310 {"MessageId", "OpenBMC.0.1.TestEventLog"}, 311 {"MessageArgs", nlohmann::json::array()}, 312 {"EventTimestamp", crow::utility::dateTimeNow()}, 313 {"Context", customText}}; 314 315 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 316 {"Id", std::to_string(eventSeqNum)}, 317 {"Name", "Event Log"}, 318 {"Events", logEntryArray}}; 319 320 this->sendEvent(msg.dump()); 321 this->eventSeqNum++; 322 } 323 324 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 325 void filterAndSendEventLogs( 326 const std::vector<EventLogObjectsType>& eventRecords) 327 { 328 nlohmann::json logEntryArray; 329 for (const EventLogObjectsType& logEntry : eventRecords) 330 { 331 const std::string& idStr = std::get<0>(logEntry); 332 const std::string& timestamp = std::get<1>(logEntry); 333 const std::string& messageID = std::get<2>(logEntry); 334 const std::string& registryName = std::get<3>(logEntry); 335 const std::string& messageKey = std::get<4>(logEntry); 336 const boost::beast::span<std::string>& messageArgs = 337 std::get<5>(logEntry); 338 339 // If registryPrefixes list is empty, don't filter events 340 // send everything. 341 if (registryPrefixes.size()) 342 { 343 auto obj = std::find(registryPrefixes.begin(), 344 registryPrefixes.end(), registryName); 345 if (obj == registryPrefixes.end()) 346 { 347 continue; 348 } 349 } 350 351 // If registryMsgIds list is empty, don't filter events 352 // send everything. 353 if (registryMsgIds.size()) 354 { 355 auto obj = std::find(registryMsgIds.begin(), 356 registryMsgIds.end(), messageKey); 357 if (obj == registryMsgIds.end()) 358 { 359 continue; 360 } 361 } 362 363 logEntryArray.push_back({}); 364 nlohmann::json& bmcLogEntry = logEntryArray.back(); 365 if (event_log::formatEventLogEntry(idStr, messageID, messageArgs, 366 timestamp, customText, 367 bmcLogEntry) != 0) 368 { 369 BMCWEB_LOG_DEBUG << "Read eventLog entry failed"; 370 continue; 371 } 372 } 373 374 if (logEntryArray.size() < 1) 375 { 376 BMCWEB_LOG_DEBUG << "No log entries available to be transferred."; 377 return; 378 } 379 380 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 381 {"Id", std::to_string(eventSeqNum)}, 382 {"Name", "Event Log"}, 383 {"Events", logEntryArray}}; 384 385 this->sendEvent(msg.dump()); 386 this->eventSeqNum++; 387 } 388 #endif 389 390 private: 391 uint64_t eventSeqNum; 392 std::string host; 393 std::string port; 394 std::string path; 395 std::string uriProto; 396 std::shared_ptr<crow::HttpClient> conn; 397 }; 398 399 class EventServiceManager 400 { 401 private: 402 EventServiceManager(const EventServiceManager&) = delete; 403 EventServiceManager& operator=(const EventServiceManager&) = delete; 404 EventServiceManager(EventServiceManager&&) = delete; 405 EventServiceManager& operator=(EventServiceManager&&) = delete; 406 407 EventServiceManager() 408 { 409 // TODO: Read the persistent data from store and populate. 410 // Populating with default. 411 enabled = true; 412 retryAttempts = 3; 413 retryTimeoutInterval = 30; // seconds 414 } 415 416 std::string lastEventTStr; 417 boost::container::flat_map<std::string, std::shared_ptr<Subscription>> 418 subscriptionsMap; 419 420 public: 421 bool enabled; 422 uint32_t retryAttempts; 423 uint32_t retryTimeoutInterval; 424 425 static EventServiceManager& getInstance() 426 { 427 static EventServiceManager handler; 428 return handler; 429 } 430 431 void updateSubscriptionData() 432 { 433 // Persist the config and subscription data. 434 // TODO: subscriptionsMap & configData need to be 435 // written to Persist store. 436 return; 437 } 438 439 std::shared_ptr<Subscription> getSubscription(const std::string& id) 440 { 441 auto obj = subscriptionsMap.find(id); 442 if (obj == subscriptionsMap.end()) 443 { 444 BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id; 445 return nullptr; 446 } 447 std::shared_ptr<Subscription> subValue = obj->second; 448 return subValue; 449 } 450 451 std::string addSubscription(const std::shared_ptr<Subscription> subValue) 452 { 453 std::srand(static_cast<uint32_t>(std::time(0))); 454 std::string id; 455 456 int retry = 3; 457 while (retry) 458 { 459 id = std::to_string(std::rand()); 460 auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); 461 if (inserted.second) 462 { 463 break; 464 } 465 --retry; 466 }; 467 468 if (retry <= 0) 469 { 470 BMCWEB_LOG_ERROR << "Failed to generate random number"; 471 return std::string(""); 472 } 473 474 updateSubscriptionData(); 475 476 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 477 if (lastEventTStr.empty()) 478 { 479 cacheLastEventTimestamp(); 480 } 481 #endif 482 return id; 483 } 484 485 bool isSubscriptionExist(const std::string& id) 486 { 487 auto obj = subscriptionsMap.find(id); 488 if (obj == subscriptionsMap.end()) 489 { 490 return false; 491 } 492 return true; 493 } 494 495 void deleteSubscription(const std::string& id) 496 { 497 auto obj = subscriptionsMap.find(id); 498 if (obj != subscriptionsMap.end()) 499 { 500 subscriptionsMap.erase(obj); 501 updateSubscriptionData(); 502 } 503 } 504 505 size_t getNumberOfSubscriptions() 506 { 507 return subscriptionsMap.size(); 508 } 509 510 std::vector<std::string> getAllIDs() 511 { 512 std::vector<std::string> idList; 513 for (const auto& it : subscriptionsMap) 514 { 515 idList.emplace_back(it.first); 516 } 517 return idList; 518 } 519 520 bool isDestinationExist(const std::string& destUrl) 521 { 522 for (const auto& it : subscriptionsMap) 523 { 524 std::shared_ptr<Subscription> entry = it.second; 525 if (entry->destinationUrl == destUrl) 526 { 527 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl; 528 return true; 529 } 530 } 531 return false; 532 } 533 534 void sendTestEventLog() 535 { 536 for (const auto& it : this->subscriptionsMap) 537 { 538 std::shared_ptr<Subscription> entry = it.second; 539 entry->sendTestEventLog(); 540 } 541 } 542 543 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 544 void cacheLastEventTimestamp() 545 { 546 std::ifstream logStream(redfishEventLogFile); 547 if (!logStream.good()) 548 { 549 BMCWEB_LOG_ERROR << " Redfish log file open failed \n"; 550 return; 551 } 552 std::string logEntry; 553 while (std::getline(logStream, logEntry)) 554 { 555 size_t space = logEntry.find_first_of(" "); 556 if (space == std::string::npos) 557 { 558 // Shouldn't enter here but lets skip it. 559 BMCWEB_LOG_DEBUG << "Invalid log entry found."; 560 continue; 561 } 562 lastEventTStr = logEntry.substr(0, space); 563 } 564 BMCWEB_LOG_DEBUG << "Last Event time stamp set: " << lastEventTStr; 565 } 566 567 void readEventLogsFromFile() 568 { 569 if (!getNumberOfSubscriptions()) 570 { 571 // no subscriptions. Just return. 572 BMCWEB_LOG_DEBUG << "No Subscriptions\n"; 573 return; 574 } 575 std::ifstream logStream(redfishEventLogFile); 576 if (!logStream.good()) 577 { 578 BMCWEB_LOG_ERROR << " Redfish log file open failed"; 579 return; 580 } 581 582 std::vector<EventLogObjectsType> eventRecords; 583 584 bool startLogCollection = false; 585 bool firstEntry = true; 586 587 std::string logEntry; 588 while (std::getline(logStream, logEntry)) 589 { 590 if (!startLogCollection) 591 { 592 if (boost::starts_with(logEntry, lastEventTStr)) 593 { 594 startLogCollection = true; 595 } 596 continue; 597 } 598 599 std::string idStr; 600 if (!event_log::getUniqueEntryID(logEntry, idStr, firstEntry)) 601 { 602 continue; 603 } 604 firstEntry = false; 605 606 std::string timestamp; 607 std::string messageID; 608 boost::beast::span<std::string> messageArgs; 609 if (event_log::getEventLogParams(logEntry, timestamp, messageID, 610 messageArgs) != 0) 611 { 612 BMCWEB_LOG_DEBUG << "Read eventLog entry params failed"; 613 continue; 614 } 615 616 std::string registryName; 617 std::string messageKey; 618 event_log::getRegistryAndMessageKey(messageID, registryName, 619 messageKey); 620 if (registryName.empty() || messageKey.empty()) 621 { 622 continue; 623 } 624 625 lastEventTStr = timestamp; 626 eventRecords.emplace_back(idStr, timestamp, messageID, registryName, 627 messageKey, messageArgs); 628 } 629 630 for (const auto& it : this->subscriptionsMap) 631 { 632 std::shared_ptr<Subscription> entry = it.second; 633 if (entry->eventFormatType == "Event") 634 { 635 entry->filterAndSendEventLogs(eventRecords); 636 } 637 } 638 } 639 640 static void watchRedfishEventLogFile() 641 { 642 if (inotifyConn == nullptr) 643 { 644 return; 645 } 646 647 static std::array<char, 1024> readBuffer; 648 649 inotifyConn->async_read_some( 650 boost::asio::buffer(readBuffer), 651 [&](const boost::system::error_code& ec, 652 const std::size_t& bytesTransferred) { 653 if (ec) 654 { 655 BMCWEB_LOG_ERROR << "Callback Error: " << ec.message(); 656 return; 657 } 658 std::size_t index = 0; 659 while ((index + sizeof(inotify_event)) <= bytesTransferred) 660 { 661 struct inotify_event event; 662 std::memcpy(&event, &readBuffer[index], 663 sizeof(inotify_event)); 664 if (event.mask == inotifyFileAction) 665 { 666 EventServiceManager::getInstance() 667 .readEventLogsFromFile(); 668 } 669 index += (sizeof(inotify_event) + event.len); 670 } 671 672 watchRedfishEventLogFile(); 673 }); 674 } 675 676 static int startEventLogMonitor(boost::asio::io_service& ioc) 677 { 678 inotifyConn = 679 std::make_shared<boost::asio::posix::stream_descriptor>(ioc); 680 int fd = inotify_init1(IN_NONBLOCK); 681 if (fd == -1) 682 { 683 BMCWEB_LOG_ERROR << "inotify_init1 failed."; 684 return -1; 685 } 686 auto wd = inotify_add_watch(fd, redfishEventLogFile, inotifyFileAction); 687 if (wd == -1) 688 { 689 BMCWEB_LOG_ERROR 690 << "inotify_add_watch failed for redfish log file."; 691 return -1; 692 } 693 694 // monitor redfish event log file 695 inotifyConn->assign(fd); 696 watchRedfishEventLogFile(); 697 698 return 0; 699 } 700 701 #endif 702 }; 703 704 } // namespace redfish 705