| /** |
| * Copyright © 2019 IBM Corporation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| #include "repository.hpp" |
| |
| #include <sys/stat.h> |
| |
| #include <fstream> |
| #include <phosphor-logging/log.hpp> |
| #include <xyz/openbmc_project/Common/File/error.hpp> |
| |
| namespace openpower |
| { |
| namespace pels |
| { |
| |
| namespace fs = std::filesystem; |
| using namespace phosphor::logging; |
| namespace file_error = sdbusplus::xyz::openbmc_project::Common::File::Error; |
| |
| constexpr size_t warningPercentage = 95; |
| |
| /** |
| * @brief Returns the amount of space the file uses on disk. |
| * |
| * This is different than just the regular size of the file. |
| * |
| * @param[in] file - The file to get the size of |
| * |
| * @return size_t The disk space the file uses |
| */ |
| size_t getFileDiskSize(const std::filesystem::path& file) |
| { |
| constexpr size_t statBlockSize = 512; |
| struct stat statData; |
| auto rc = stat(file.c_str(), &statData); |
| if (rc != 0) |
| { |
| auto e = errno; |
| std::string msg = "call to stat() failed on " + file.native() + |
| " with errno " + std::to_string(e); |
| log<level::ERR>(msg.c_str()); |
| abort(); |
| } |
| |
| return statData.st_blocks * statBlockSize; |
| } |
| |
| Repository::Repository(const std::filesystem::path& basePath, size_t repoSize, |
| size_t maxNumPELs) : |
| _logPath(basePath / "logs"), |
| _maxRepoSize(repoSize), _maxNumPELs(maxNumPELs), |
| _archivePath(basePath / "logs" / "archive") |
| { |
| if (!fs::exists(_logPath)) |
| { |
| fs::create_directories(_logPath); |
| } |
| |
| if (!fs::exists(_archivePath)) |
| { |
| fs::create_directories(_archivePath); |
| } |
| |
| restore(); |
| } |
| |
| void Repository::restore() |
| { |
| for (auto& dirEntry : fs::directory_iterator(_logPath)) |
| { |
| try |
| { |
| if (!fs::is_regular_file(dirEntry.path())) |
| { |
| continue; |
| } |
| |
| std::ifstream file{dirEntry.path()}; |
| std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| file.close(); |
| |
| PEL pel{data}; |
| if (pel.valid()) |
| { |
| // If the host hasn't acked it, reset the host state so |
| // it will get sent up again. |
| if (pel.hostTransmissionState() == TransmissionState::sent) |
| { |
| pel.setHostTransmissionState(TransmissionState::newPEL); |
| try |
| { |
| write(pel, dirEntry.path()); |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>( |
| "Failed to save PEL after updating host state", |
| entry("PELID=0x%X", pel.id())); |
| } |
| } |
| |
| PELAttributes attributes{dirEntry.path(), |
| getFileDiskSize(dirEntry.path()), |
| pel.privateHeader().creatorID(), |
| pel.userHeader().severity(), |
| pel.userHeader().actionFlags(), |
| pel.hostTransmissionState(), |
| pel.hmcTransmissionState()}; |
| |
| using pelID = LogID::Pel; |
| using obmcID = LogID::Obmc; |
| _pelAttributes.emplace( |
| LogID(pelID(pel.id()), obmcID(pel.obmcLogID())), |
| attributes); |
| |
| updateRepoStats(attributes, true); |
| } |
| else |
| { |
| log<level::ERR>( |
| "Found invalid PEL file while restoring. Removing.", |
| entry("FILENAME=%s", dirEntry.path().c_str())); |
| fs::remove(dirEntry.path()); |
| } |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("Hit exception while restoring PEL File", |
| entry("FILENAME=%s", dirEntry.path().c_str()), |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| |
| // Get size of archive folder |
| for (auto& dirEntry : fs::directory_iterator(_archivePath)) |
| { |
| _archiveSize += getFileDiskSize(dirEntry); |
| } |
| } |
| |
| std::string Repository::getPELFilename(uint32_t pelID, const BCDTime& time) |
| { |
| char name[50]; |
| sprintf(name, "%.2X%.2X%.2X%.2X%.2X%.2X%.2X%.2X_%.8X", time.yearMSB, |
| time.yearLSB, time.month, time.day, time.hour, time.minutes, |
| time.seconds, time.hundredths, pelID); |
| return std::string{name}; |
| } |
| |
| void Repository::add(std::unique_ptr<PEL>& pel) |
| { |
| pel->setHostTransmissionState(TransmissionState::newPEL); |
| pel->setHMCTransmissionState(TransmissionState::newPEL); |
| |
| auto path = _logPath / getPELFilename(pel->id(), pel->commitTime()); |
| |
| write(*(pel.get()), path); |
| |
| PELAttributes attributes{path, |
| getFileDiskSize(path), |
| pel->privateHeader().creatorID(), |
| pel->userHeader().severity(), |
| pel->userHeader().actionFlags(), |
| pel->hostTransmissionState(), |
| pel->hmcTransmissionState()}; |
| |
| using pelID = LogID::Pel; |
| using obmcID = LogID::Obmc; |
| _pelAttributes.emplace(LogID(pelID(pel->id()), obmcID(pel->obmcLogID())), |
| attributes); |
| |
| _lastPelID = pel->id(); |
| |
| updateRepoStats(attributes, true); |
| |
| processAddCallbacks(*pel); |
| } |
| |
| void Repository::write(const PEL& pel, const fs::path& path) |
| { |
| std::ofstream file{path, std::ios::binary}; |
| |
| if (!file.good()) |
| { |
| // If this fails, the filesystem is probably full so it isn't like |
| // we could successfully create yet another error log here. |
| auto e = errno; |
| fs::remove(path); |
| log<level::ERR>("Unable to open PEL file for writing", |
| entry("ERRNO=%d", e), entry("PATH=%s", path.c_str())); |
| throw file_error::Open(); |
| } |
| |
| auto data = pel.data(); |
| file.write(reinterpret_cast<const char*>(data.data()), data.size()); |
| |
| if (file.fail()) |
| { |
| // Same note as above about not being able to create an error log |
| // for this case even if we wanted. |
| auto e = errno; |
| file.close(); |
| fs::remove(path); |
| log<level::ERR>("Unable to write PEL file", entry("ERRNO=%d", e), |
| entry("PATH=%s", path.c_str())); |
| throw file_error::Write(); |
| } |
| } |
| |
| std::optional<Repository::LogID> Repository::remove(const LogID& id) |
| { |
| auto pel = findPEL(id); |
| if (pel == _pelAttributes.end()) |
| { |
| return std::nullopt; |
| } |
| |
| LogID actualID = pel->first; |
| updateRepoStats(pel->second, false); |
| |
| log<level::DEBUG>("Removing PEL from repository", |
| entry("PEL_ID=0x%X", actualID.pelID.id), |
| entry("OBMC_LOG_ID=%d", actualID.obmcID.id)); |
| |
| if (fs::exists(pel->second.path)) |
| { |
| // Check for existense of new archive folder |
| if (!fs::exists(_archivePath)) |
| { |
| fs::create_directories(_archivePath); |
| } |
| |
| // Move log file to archive folder |
| auto fileName = _archivePath / pel->second.path.filename(); |
| fs::rename(pel->second.path, fileName); |
| |
| // Update size of file |
| _archiveSize += getFileDiskSize(fileName); |
| } |
| |
| _pelAttributes.erase(pel); |
| |
| processDeleteCallbacks(actualID.pelID.id); |
| |
| return actualID; |
| } |
| |
| std::optional<std::vector<uint8_t>> Repository::getPELData(const LogID& id) |
| { |
| auto pel = findPEL(id); |
| if (pel != _pelAttributes.end()) |
| { |
| std::ifstream file{pel->second.path.c_str()}; |
| if (!file.good()) |
| { |
| auto e = errno; |
| log<level::ERR>("Unable to open PEL file", entry("ERRNO=%d", e), |
| entry("PATH=%s", pel->second.path.c_str())); |
| throw file_error::Open(); |
| } |
| |
| std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| return data; |
| } |
| |
| return std::nullopt; |
| } |
| |
| std::optional<sdbusplus::message::unix_fd> Repository::getPELFD(const LogID& id) |
| { |
| auto pel = findPEL(id); |
| if (pel != _pelAttributes.end()) |
| { |
| FILE* fp = fopen(pel->second.path.c_str(), "rb"); |
| |
| if (fp == nullptr) |
| { |
| auto e = errno; |
| log<level::ERR>("Unable to open PEL File", entry("ERRNO=%d", e), |
| entry("PATH=%s", pel->second.path.c_str())); |
| throw file_error::Open(); |
| } |
| |
| // Must leave the file open here. It will be closed by sdbusplus |
| // when it sends it back over D-Bus. |
| |
| return fileno(fp); |
| } |
| return std::nullopt; |
| } |
| |
| void Repository::for_each(ForEachFunc func) const |
| { |
| for (const auto& [id, attributes] : _pelAttributes) |
| { |
| std::ifstream file{attributes.path}; |
| |
| if (!file.good()) |
| { |
| auto e = errno; |
| log<level::ERR>("Repository::for_each: Unable to open PEL file", |
| entry("ERRNO=%d", e), |
| entry("PATH=%s", attributes.path.c_str())); |
| continue; |
| } |
| |
| std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| file.close(); |
| |
| PEL pel{data}; |
| |
| try |
| { |
| if (func(pel)) |
| { |
| break; |
| } |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("Repository::for_each function exception", |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| } |
| |
| void Repository::processAddCallbacks(const PEL& pel) const |
| { |
| for (auto& [name, func] : _addSubscriptions) |
| { |
| try |
| { |
| func(pel); |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("PEL Repository add callback exception", |
| entry("NAME=%s", name.c_str()), |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| } |
| |
| void Repository::processDeleteCallbacks(uint32_t id) const |
| { |
| for (auto& [name, func] : _deleteSubscriptions) |
| { |
| try |
| { |
| func(id); |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("PEL Repository delete callback exception", |
| entry("NAME=%s", name.c_str()), |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| } |
| |
| std::optional<std::reference_wrapper<const Repository::PELAttributes>> |
| Repository::getPELAttributes(const LogID& id) const |
| { |
| auto pel = findPEL(id); |
| if (pel != _pelAttributes.end()) |
| { |
| return pel->second; |
| } |
| |
| return std::nullopt; |
| } |
| |
| void Repository::setPELHostTransState(uint32_t pelID, TransmissionState state) |
| { |
| LogID id{LogID::Pel{pelID}}; |
| auto attr = std::find_if(_pelAttributes.begin(), _pelAttributes.end(), |
| [&id](const auto& a) { return a.first == id; }); |
| |
| if ((attr != _pelAttributes.end()) && (attr->second.hostState != state)) |
| { |
| PELUpdateFunc func = [state](PEL& pel) { |
| pel.setHostTransmissionState(state); |
| }; |
| |
| try |
| { |
| updatePEL(attr->second.path, func); |
| |
| attr->second.hostState = state; |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("Unable to update PEL host transmission state", |
| entry("PATH=%s", attr->second.path.c_str()), |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| } |
| |
| void Repository::setPELHMCTransState(uint32_t pelID, TransmissionState state) |
| { |
| LogID id{LogID::Pel{pelID}}; |
| auto attr = std::find_if(_pelAttributes.begin(), _pelAttributes.end(), |
| [&id](const auto& a) { return a.first == id; }); |
| |
| if ((attr != _pelAttributes.end()) && (attr->second.hmcState != state)) |
| { |
| PELUpdateFunc func = [state](PEL& pel) { |
| pel.setHMCTransmissionState(state); |
| }; |
| |
| try |
| { |
| updatePEL(attr->second.path, func); |
| |
| attr->second.hmcState = state; |
| } |
| catch (std::exception& e) |
| { |
| log<level::ERR>("Unable to update PEL HMC transmission state", |
| entry("PATH=%s", attr->second.path.c_str()), |
| entry("ERROR=%s", e.what())); |
| } |
| } |
| } |
| |
| void Repository::updatePEL(const fs::path& path, PELUpdateFunc updateFunc) |
| { |
| std::ifstream file{path}; |
| std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| file.close(); |
| |
| PEL pel{data}; |
| |
| if (pel.valid()) |
| { |
| updateFunc(pel); |
| |
| write(pel, path); |
| } |
| else |
| { |
| throw std::runtime_error( |
| "Unable to read a valid PEL when trying to update it"); |
| } |
| } |
| |
| bool Repository::isServiceableSev(const PELAttributes& pel) |
| { |
| auto sevType = static_cast<SeverityType>(pel.severity & 0xF0); |
| auto sevPVEntry = |
| pel_values::findByValue(pel.severity, pel_values::severityValues); |
| std::string sevName = std::get<pel_values::registryNamePos>(*sevPVEntry); |
| |
| bool check1 = (sevType == SeverityType::predictive) || |
| (sevType == SeverityType::unrecoverable) || |
| (sevType == SeverityType::critical); |
| |
| bool check2 = ((sevType == SeverityType::recovered) || |
| (sevName == "symptom_recovered")) && |
| !pel.actionFlags.test(hiddenFlagBit); |
| |
| bool check3 = (sevName == "symptom_predictive") || |
| (sevName == "symptom_unrecoverable") || |
| (sevName == "symptom_critical"); |
| |
| return check1 || check2 || check3; |
| } |
| |
| void Repository::updateRepoStats(const PELAttributes& pel, bool pelAdded) |
| { |
| auto isServiceable = Repository::isServiceableSev(pel); |
| auto bmcPEL = CreatorID::openBMC == static_cast<CreatorID>(pel.creator); |
| |
| auto adjustSize = [pelAdded, &pel](auto& runningSize) { |
| if (pelAdded) |
| { |
| runningSize += pel.sizeOnDisk; |
| } |
| else |
| { |
| runningSize = std::max(static_cast<int64_t>(runningSize) - |
| static_cast<int64_t>(pel.sizeOnDisk), |
| static_cast<int64_t>(0)); |
| } |
| }; |
| |
| adjustSize(_sizes.total); |
| |
| if (bmcPEL) |
| { |
| adjustSize(_sizes.bmc); |
| if (isServiceable) |
| { |
| adjustSize(_sizes.bmcServiceable); |
| } |
| else |
| { |
| adjustSize(_sizes.bmcInfo); |
| } |
| } |
| else |
| { |
| adjustSize(_sizes.nonBMC); |
| if (isServiceable) |
| { |
| adjustSize(_sizes.nonBMCServiceable); |
| } |
| else |
| { |
| adjustSize(_sizes.nonBMCInfo); |
| } |
| } |
| } |
| |
| bool Repository::sizeWarning() |
| { |
| std::error_code ec; |
| |
| if ((_archiveSize > 0) && ((_sizes.total + _archiveSize) > |
| ((_maxRepoSize * warningPercentage) / 100))) |
| { |
| log<level::INFO>( |
| "Repository::sizeWarning function:Deleting the files in archive"); |
| |
| for (const auto& dirEntry : fs::directory_iterator(_archivePath)) |
| { |
| fs::remove(dirEntry.path(), ec); |
| if (ec) |
| { |
| log<level::INFO>( |
| "Repository::sizeWarning function:Could not delete " |
| "a file in PEL archive", |
| entry("FILENAME=%s", dirEntry.path().c_str())); |
| } |
| } |
| |
| _archiveSize = 0; |
| } |
| |
| return (_sizes.total > (_maxRepoSize * warningPercentage / 100)) || |
| (_pelAttributes.size() > _maxNumPELs); |
| } |
| |
| std::vector<Repository::AttributesReference> |
| Repository::getAllPELAttributes(SortOrder order) const |
| { |
| std::vector<Repository::AttributesReference> attributes; |
| |
| std::for_each( |
| _pelAttributes.begin(), _pelAttributes.end(), |
| [&attributes](auto& pelEntry) { attributes.push_back(pelEntry); }); |
| |
| std::sort(attributes.begin(), attributes.end(), |
| [order](const auto& left, const auto& right) { |
| if (order == SortOrder::ascending) |
| { |
| return left.get().second.path < right.get().second.path; |
| } |
| return left.get().second.path > right.get().second.path; |
| }); |
| |
| return attributes; |
| } |
| |
| std::vector<uint32_t> Repository::prune() |
| { |
| std::vector<uint32_t> obmcLogIDs; |
| std::string msg = "Pruning PEL repository that takes up " + |
| std::to_string(_sizes.total) + " bytes and has " + |
| std::to_string(_pelAttributes.size()) + " PELs"; |
| log<level::INFO>(msg.c_str()); |
| |
| // Set up the 5 functions to check if the PEL category |
| // is still over its limits. |
| |
| // BMC informational PELs should only take up 15% |
| IsOverLimitFunc overBMCInfoLimit = [this]() { |
| return _sizes.bmcInfo > _maxRepoSize * 15 / 100; |
| }; |
| |
| // BMC non informational PELs should only take up 30% |
| IsOverLimitFunc overBMCNonInfoLimit = [this]() { |
| return _sizes.bmcServiceable > _maxRepoSize * 30 / 100; |
| }; |
| |
| // Non BMC informational PELs should only take up 15% |
| IsOverLimitFunc overNonBMCInfoLimit = [this]() { |
| return _sizes.nonBMCInfo > _maxRepoSize * 15 / 100; |
| }; |
| |
| // Non BMC non informational PELs should only take up 15% |
| IsOverLimitFunc overNonBMCNonInfoLimit = [this]() { |
| return _sizes.nonBMCServiceable > _maxRepoSize * 30 / 100; |
| }; |
| |
| // Bring the total number of PELs down to 80% of the max |
| IsOverLimitFunc tooManyPELsLimit = [this]() { |
| return _pelAttributes.size() > _maxNumPELs * 80 / 100; |
| }; |
| |
| // Set up the functions to determine which category a PEL is in. |
| // TODO: Return false in these functions if a PEL caused a guard record. |
| |
| // A BMC informational PEL |
| IsPELTypeFunc isBMCInfo = [](const PELAttributes& pel) { |
| return (CreatorID::openBMC == static_cast<CreatorID>(pel.creator)) && |
| !Repository::isServiceableSev(pel); |
| }; |
| |
| // A BMC non informational PEL |
| IsPELTypeFunc isBMCNonInfo = [](const PELAttributes& pel) { |
| return (CreatorID::openBMC == static_cast<CreatorID>(pel.creator)) && |
| Repository::isServiceableSev(pel); |
| }; |
| |
| // A non BMC informational PEL |
| IsPELTypeFunc isNonBMCInfo = [](const PELAttributes& pel) { |
| return (CreatorID::openBMC != static_cast<CreatorID>(pel.creator)) && |
| !Repository::isServiceableSev(pel); |
| }; |
| |
| // A non BMC non informational PEL |
| IsPELTypeFunc isNonBMCNonInfo = [](const PELAttributes& pel) { |
| return (CreatorID::openBMC != static_cast<CreatorID>(pel.creator)) && |
| Repository::isServiceableSev(pel); |
| }; |
| |
| // When counting PELs, count every PEL |
| IsPELTypeFunc isAnyPEL = [](const PELAttributes& /*pel*/) { return true; }; |
| |
| // Check all 4 categories, which will result in at most 90% |
| // usage (15 + 30 + 15 + 30). |
| removePELs(overBMCInfoLimit, isBMCInfo, obmcLogIDs); |
| removePELs(overBMCNonInfoLimit, isBMCNonInfo, obmcLogIDs); |
| removePELs(overNonBMCInfoLimit, isNonBMCInfo, obmcLogIDs); |
| removePELs(overNonBMCNonInfoLimit, isNonBMCNonInfo, obmcLogIDs); |
| |
| // After the above pruning check if there are still too many PELs, |
| // which can happen depending on PEL sizes. |
| if (_pelAttributes.size() > _maxNumPELs) |
| { |
| removePELs(tooManyPELsLimit, isAnyPEL, obmcLogIDs); |
| } |
| |
| if (!obmcLogIDs.empty()) |
| { |
| std::string msg = "Number of PELs removed to save space: " + |
| std::to_string(obmcLogIDs.size()); |
| log<level::INFO>(msg.c_str()); |
| } |
| |
| return obmcLogIDs; |
| } |
| |
| void Repository::removePELs(IsOverLimitFunc& isOverLimit, |
| IsPELTypeFunc& isPELType, |
| std::vector<uint32_t>& removedBMCLogIDs) |
| { |
| if (!isOverLimit()) |
| { |
| return; |
| } |
| |
| auto attributes = getAllPELAttributes(SortOrder::ascending); |
| |
| // Make 4 passes on the PELs, stopping as soon as isOverLimit |
| // returns false. |
| // Pass 1: only delete HMC acked PELs |
| // Pass 2: only delete OS acked PELs |
| // Pass 3: only delete PHYP sent PELs |
| // Pass 4: delete all PELs |
| static const std::vector<std::function<bool(const PELAttributes& pel)>> |
| stateChecks{[](const auto& pel) { |
| return pel.hmcState == TransmissionState::acked; |
| }, |
| |
| [](const auto& pel) { |
| return pel.hostState == TransmissionState::acked; |
| }, |
| |
| [](const auto& pel) { |
| return pel.hostState == TransmissionState::sent; |
| }, |
| |
| [](const auto& /*pel*/) { return true; }}; |
| |
| for (const auto& stateCheck : stateChecks) |
| { |
| for (auto it = attributes.begin(); it != attributes.end();) |
| { |
| const auto& pel = it->get(); |
| if (isPELType(pel.second) && stateCheck(pel.second)) |
| { |
| auto removedID = pel.first.obmcID.id; |
| remove(pel.first); |
| |
| removedBMCLogIDs.push_back(removedID); |
| |
| attributes.erase(it); |
| |
| if (!isOverLimit()) |
| { |
| break; |
| } |
| } |
| else |
| { |
| ++it; |
| } |
| } |
| |
| if (!isOverLimit()) |
| { |
| break; |
| } |
| } |
| } |
| |
| } // namespace pels |
| } // namespace openpower |