blob: c2df58b013b91d62316528c4a28c32278862bdf0 [file] [log] [blame]
#include "external_storer.hpp"
#include "app.hpp"
#include "error_messages.hpp"
#include "openbmc_dbus_rest.hpp"
#include "query.hpp"
#include "registries/privilege_registry.hpp"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <sstream>
namespace external_storer
{
std::filesystem::path safeAppend(const std::filesystem::path& a,
const std::filesystem::path& b)
{
std::filesystem::path result{a};
// Unfortunately, a / b returns surprising/wrong results if b is absolute
if (b.is_absolute())
{
// The absolute path already starts with necessary directory separator
result += b;
return result;
}
result /= b;
return result;
}
std::filesystem::path Hook::locBase() const
{
return safeAppend(redfishPrefix, pathBase);
}
std::filesystem::path Hook::locInstance(const std::string& instance) const
{
return safeAppend(locBase(), instance);
}
std::filesystem::path Hook::locMiddle(const std::string& instance) const
{
// The middle component is optional, some schemas might not need it
if (pathMiddle.empty())
{
return locInstance(instance);
}
return safeAppend(locInstance(instance), pathMiddle);
}
std::filesystem::path Hook::locEntry(const std::string& instance,
const std::string& entry) const
{
return safeAppend(locMiddle(instance), entry);
}
std::filesystem::path Hook::locToFileDir(const std::filesystem::path& loc) const
{
return safeAppend(pathPrefix, loc);
}
std::filesystem::path
Hook::locToFileJson(const std::filesystem::path& loc) const
{
// Safe to use / operator here, jsonFilename constant always relative
return locToFileDir(loc) / jsonFilename;
}
// Helper function to get size of an open file, also rewinds file
// Uses the C library (not C++) to be called from fileToString
std::optional<size_t> fileToSize(FILE* f)
{
int ret = std::fseek(f, 0L, SEEK_END);
if (ret != 0)
{
int err = errno;
BMCWEB_LOG_ERROR << "File failed to seek: " << std::strerror(err);
return std::nullopt;
}
// Assumes file has been opened in binary mode
int64_t rawSize = ftell(f);
if (rawSize < 0)
{
int err = errno;
BMCWEB_LOG_ERROR << "File failed to tell: " << std::strerror(err);
return std::nullopt;
}
ret = std::fseek(f, 0L, SEEK_SET);
if (ret != 0)
{
int err = errno;
BMCWEB_LOG_ERROR << "File failed to seek: " << std::strerror(err);
return std::nullopt;
}
// Historical C library API legacy: long versus size_t
auto size = static_cast<size_t>(rawSize);
BMCWEB_LOG_DEBUG << "File open success: " << size << " bytes";
return {size};
}
// Use the C library (not C++) to read entire file into a C++ string first
// Tested earlier to be faster than nlohmann::json::parse() of std::ifstream
std::optional<std::string> fileToString(FILE* f)
{
auto sizeOpt = fileToSize(f);
if (!sizeOpt.has_value())
{
// Helper function has already printed an error message
return std::nullopt;
}
size_t fileSize = *sizeOpt;
if (fileSize == 0)
{
BMCWEB_LOG_ERROR << "File is empty";
return std::nullopt;
}
if (fileSize > maxFileSize)
{
BMCWEB_LOG_ERROR << "File is too large: " << fileSize << "/"
<< maxFileSize;
return std::nullopt;
}
std::string empty;
std::optional<std::string> contentOpt{empty};
// Avoid making expensive copies of string after it gets larger
std::string& content = *contentOpt;
// Preallocate the buffer to fit the file size
content.resize(fileSize);
// Read the whole file in one go
// C++11 no longer disallows data() from being written to
size_t readSize = fread(content.data(), 1, fileSize, f);
// Read of 0 must be error, because empty file already excluded earlier
if (readSize == 0)
{
int err = errno;
BMCWEB_LOG_ERROR << "File read error: " << std::strerror(err);
return std::nullopt;
}
// Consider a short read to also be an error
if (readSize != fileSize)
{
BMCWEB_LOG_ERROR << "File read size mismatch: " << readSize << "/"
<< fileSize;
return std::nullopt;
}
BMCWEB_LOG_DEBUG << "File read success: " << readSize << " bytes";
return contentOpt;
}
std::optional<nlohmann::json>
readJsonFile(const std::filesystem::path& filename)
{
// Open in binary mode to ensure accurate size measurement
FILE* f = std::fopen(filename.c_str(), "rb");
if (f == nullptr)
{
int err = errno;
BMCWEB_LOG_ERROR << "File " << filename
<< " failed to open: " << std::strerror(err);
return std::nullopt;
}
// While we have the file open, use helper function to read it
auto bufferOpt = fileToString(f);
int ret = std::fclose(f);
if (ret != 0)
{
int err = errno;
BMCWEB_LOG_ERROR << "File " << filename
<< " failed to close: " << std::strerror(err);
return std::nullopt;
}
// All further error checking takes place after the close
if (!bufferOpt.has_value())
{
// Helper function has already printed an error message
return std::nullopt;
}
const std::string& buffer = *bufferOpt;
// Must supply 3rd argument to avoid throwing exceptions
nlohmann::json content = nlohmann::json::parse(buffer, nullptr, false);
if (content.is_discarded())
{
BMCWEB_LOG_ERROR << "File " << filename << " not valid JSON";
return std::nullopt;
}
if (!(content.is_object()))
{
BMCWEB_LOG_ERROR << "File " << filename << " not JSON dictionary";
return std::nullopt;
}
BMCWEB_LOG_DEBUG << "File JSON success: " << filename;
return {content};
}
std::optional<std::size_t> writeJsonFile(const std::filesystem::path& filename,
const nlohmann::json& content)
{
std::stringstream stream;
// Must supply 4th argument to avoid throwing exceptions
stream << content.dump(-1, ' ', false,
nlohmann::json::error_handler_t::replace);
std::string buffer = stream.str();
size_t fileSize = buffer.size();
if (fileSize > maxFileSize)
{
BMCWEB_LOG_ERROR << "File is too large: " << fileSize << "/"
<< maxFileSize;
return std::nullopt;
}
BMCWEB_LOG_DEBUG << "Output write size: " << fileSize << " bytes";
std::ofstream output;
output.open(filename, std::ofstream::trunc);
if (!output)
{
int err = errno;
BMCWEB_LOG_ERROR << "Error opening " << filename
<< " output: " << strerror(err);
return std::nullopt;
}
output.write(buffer.data(), static_cast<std::streamsize>(fileSize));
bool writeGood = output.good();
if (!writeGood)
{
// Do not return here, need to defer until after the close
int err = errno;
BMCWEB_LOG_ERROR << "Error writing " << filename
<< " output: " << strerror(err);
}
// Always do this, no matter what, even if write failed
output.close();
bool closeGood = output.good();
// This is the deferred error return from write() above
if (!writeGood)
{
// The errno from write() already printed
return std::nullopt;
}
if (!closeGood)
{
int err = errno;
BMCWEB_LOG_ERROR << "Error closing " << filename
<< " output: " << strerror(err);
return std::nullopt;
}
BMCWEB_LOG_DEBUG << "Output write success: " << filename;
return {fileSize};
}
// The "proposedName" should be a basename, with no directory separators
// Conservative filename rules to begin with, can relax later if needed
bool validateFilename(const std::filesystem::path& proposedName)
{
if (!(crow::openbmc_mapper::validateFilename(proposedName)))
{
BMCWEB_LOG_ERROR << "Filename contains invalid characters";
return false;
}
return true;
}
bool validateFilename(const std::filesystem::path& name,
const std::vector<std::string>& denyList)
{
if (!(validateFilename(name)))
{
// Error message has already been printed
return false;
}
// Must not be within the denylist
if (std::find(denyList.begin(), denyList.end(), name) != denyList.end())
{
BMCWEB_LOG_ERROR << "Filename " << name << " is reserved";
return false;
}
return true;
}
std::string extractId(const nlohmann::json& content)
{
std::string id;
if (content.is_object())
{
auto foundId = content.find("Id");
if (foundId != content.end())
{
if (foundId->is_string())
{
id = foundId.value();
if (!(id.empty()))
{
return id;
}
}
}
}
boost::uuids::random_generator gen;
// Roll a random UUID for server-assigned ID
id = boost::uuids::to_string(gen());
BMCWEB_LOG_INFO << "Generated UUID " << id;
return id;
}
void stripFieldsId(nlohmann::json& content)
{
if (!(content.is_object()))
{
return;
}
// No need, this is already implied by the filename on disk
auto foundId = content.find("Id");
if (foundId != content.end())
{
content.erase(foundId);
}
// No need, this will be dynamically built when output to user
auto foundOdataId = content.find("@odata.id");
if (foundOdataId != content.end())
{
content.erase(foundOdataId);
}
}
void stripFieldsMembers(nlohmann::json& content)
{
if (!(content.is_object()))
{
return;
}
// Entries must be added one at a time, using separate POST commands
auto foundMembers = content.find("Members");
if (foundMembers != content.end())
{
content.erase(foundMembers);
}
// No need, this will be dynamically built when output to user
auto foundCount = content.find("Members@odata.count");
if (foundCount != content.end())
{
content.erase(foundCount);
}
}
void insertResponseLocation(crow::Response& response,
const std::string& location)
{
// Add Location to header
response.addHeader(boost::beast::http::field::location, location);
// Add Location to body, but must dig through schema first
if (!(response.jsonValue.is_object()))
{
BMCWEB_LOG_ERROR << "No Location because not object";
return;
}
// ExtendedInfo must already be an array of at least 1 element (object)
auto ei = response.jsonValue.find("@Message.ExtendedInfo");
if (ei == response.jsonValue.end())
{
BMCWEB_LOG_ERROR << "No Location because no ExtendedInfo";
return;
}
if (!(ei->is_array()))
{
BMCWEB_LOG_ERROR << "No Location because ExtendedInfo not array";
return;
}
if (ei->empty())
{
BMCWEB_LOG_ERROR << "No Location because ExtendedInfo empty";
return;
}
if (!((*ei)[0].is_object()))
{
BMCWEB_LOG_ERROR
<< "No Location because ExtendedInfo element not object";
return;
}
// MessageArgs must be an array, create if it does not already exist
auto ma = (*ei)[0].find("MessageArgs");
if (ma == (*ei)[0].end())
{
(*ei)[0]["MessageArgs"] = nlohmann::json::array();
ma = (*ei)[0].find("MessageArgs");
}
if (!(ma->is_array()))
{
BMCWEB_LOG_ERROR << "No Location because MessageArgs not array";
return;
}
ma->emplace_back(location);
}
void Hook::handleCreateInstance(
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
{
nlohmann::json content;
content = nlohmann::json::parse(req.body(), nullptr, false);
if (content.is_discarded())
{
BMCWEB_LOG_ERROR << "Uploaded content not JSON";
redfish::messages::malformedJSON(asyncResp->res);
return;
}
if (!(content.is_object()))
{
BMCWEB_LOG_ERROR << "Uploaded JSON type not a dictionary";
redfish::messages::unrecognizedRequestBody(asyncResp->res);
return;
}
std::string idInstance = extractId(content);
stripFieldsId(content);
auto innerContent = nlohmann::json::object();
if (!(pathMiddle.empty()))
{
// Promote the inner layer to its own JSON object
auto foundMiddle = content.find(pathMiddle);
if (foundMiddle != content.end())
{
innerContent = foundMiddle.value();
content.erase(foundMiddle);
if (!(innerContent.is_object()))
{
BMCWEB_LOG_ERROR << "Interior JSON type not a dictionary";
redfish::messages::unrecognizedRequestBody(asyncResp->res);
return;
}
// Also trim "Id" and "@odata.id" from the inner layer
stripFieldsId(innerContent);
// Trim "Members" as well, user not allowed bulk upload yet
stripFieldsMembers(innerContent);
}
}
if (!(validateFilename(idInstance, denyList)))
{
BMCWEB_LOG_ERROR << "Uploaded instance ID not acceptable";
redfish::messages::actionParameterValueFormatError(
asyncResp->res, idInstance, "Id", "POST");
return;
}
std::filesystem::path outerUrl = locInstance(idInstance);
std::filesystem::path outerDir = locToFileDir(outerUrl);
std::filesystem::path outerFilename = locToFileJson(outerUrl);
std::filesystem::path innerUrl = locMiddle(idInstance);
std::filesystem::path innerDir = locToFileDir(innerUrl);
std::filesystem::path innerFilename = locToFileJson(innerUrl);
// If no middle keyword, then no need to create multiple layers
if (pathMiddle.empty())
{
innerDir = outerDir;
}
std::error_code ec;
BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " checking "
<< outerDir;
bool outerExists = std::filesystem::exists(outerFilename, ec);
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << outerFilename
<< " duplicate: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
// If no middle keyword, only outer file necessary to declare dupe
bool innerExists = true;
if (!pathMiddle.empty())
{
innerExists = std::filesystem::exists(innerFilename, ec);
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << innerFilename
<< " duplicate: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
}
// It is only considered a dupe error if both files already exist
if (outerExists && innerExists)
{
BMCWEB_LOG_ERROR << "Uploaded instance ID already exists on system";
redfish::messages::resourceAlreadyExists(asyncResp->res, "String", "Id",
idInstance);
return;
}
BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " making "
<< innerDir;
std::filesystem::create_directories(innerDir, ec);
if (ec)
{
BMCWEB_LOG_ERROR << "Problem making " << innerDir
<< " directories: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " writing "
<< outerFilename;
if (!(writeJsonFile(outerFilename, content).has_value()))
{
BMCWEB_LOG_ERROR << "Problem writing file " << outerFilename;
redfish::messages::operationFailed(asyncResp->res);
return;
}
if (!(pathMiddle.empty()))
{
BMCWEB_LOG_DEBUG << "Create instance " << idInstance << "/"
<< pathMiddle << " writing " << innerFilename;
if (!(writeJsonFile(innerFilename, innerContent).has_value()))
{
BMCWEB_LOG_ERROR << "Problem writing file " << innerFilename;
redfish::messages::operationFailed(asyncResp->res);
return;
}
}
redfish::messages::created(asyncResp->res);
insertResponseLocation(asyncResp->res, outerUrl);
}
void Hook::handleCreateMiddle(
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& idInstance)
{
// Before doing anything with filesystem, validate naming restrictions
if (!(validateFilename(idInstance, denyList)))
{
BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable";
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
nlohmann::json content;
// Keep this in sync with post-I/O validation in readJsonFile()
content = nlohmann::json::parse(req.body(), nullptr, false);
if (content.is_discarded())
{
BMCWEB_LOG_ERROR << "Uploaded content not JSON";
redfish::messages::malformedJSON(asyncResp->res);
return;
}
if (!(content.is_object()))
{
BMCWEB_LOG_ERROR << "Uploaded JSON type not a dictionary";
redfish::messages::unrecognizedRequestBody(asyncResp->res);
return;
}
std::string idEntry = extractId(content);
// Unlike instance, no need to do a second layer of trimming underneath
stripFieldsId(content);
// Unlike instance, names on denyList are perfectly OK for entry
if (!(validateFilename(idEntry)))
{
BMCWEB_LOG_ERROR << "Uploaded entry ID not acceptable";
redfish::messages::actionParameterValueFormatError(
asyncResp->res, idEntry, "Id", "POST");
return;
}
std::filesystem::path outerUrl = locInstance(idInstance);
std::filesystem::path outerDir = locToFileDir(outerUrl);
std::filesystem::path entryUrl = locEntry(idInstance, idEntry);
std::filesystem::path entryDir = locToFileDir(entryUrl);
std::filesystem::path entryFilename = locToFileJson(entryUrl);
std::error_code ec;
// The instance must already have been created earlier
BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " checking "
<< outerDir;
if (!(std::filesystem::exists(outerDir, ec)))
{
BMCWEB_LOG_ERROR << "Cannot add entry to nonexistent instance "
<< idInstance;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << outerDir
<< " existence: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " making " << entryDir;
std::filesystem::create_directories(entryDir, ec);
if (ec)
{
BMCWEB_LOG_ERROR << "Problem making " << entryDir
<< " directories: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " writing "
<< entryFilename;
if (std::filesystem::exists(entryFilename, ec))
{
BMCWEB_LOG_ERROR << "Uploaded entry ID already exists within instance";
redfish::messages::resourceAlreadyExists(asyncResp->res, "String", "Id",
idEntry);
return;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << entryFilename
<< " duplicate: " << ec.message();
redfish::messages::operationFailed(asyncResp->res);
return;
}
if (!(writeJsonFile(entryFilename, content).has_value()))
{
BMCWEB_LOG_ERROR << "Problem writing file " << entryFilename;
redfish::messages::operationFailed(asyncResp->res);
return;
}
redfish::messages::created(asyncResp->res);
insertResponseLocation(asyncResp->res, entryUrl);
}
void Hook::handleCreateEntry(
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& idInstance, const std::string& keywordMiddle)
{
// Validate the middle path component in URL is the expected constant
if (keywordMiddle != pathMiddle)
{
BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
// This handler has the same function as if the middle were omitted
handleCreateMiddle(req, asyncResp, idInstance);
}
// Given a dir, list its subdirs, but only those subdirs which are themselves
// directories, and contain an "index.json" file within them.
// Those "index.json" files are only checked for existence, nothing more.
std::vector<std::filesystem::path>
listJsonDirs(const std::filesystem::path& dir)
{
std::vector<std::filesystem::path> files;
std::error_code ec;
// If containing directory not found, there can be no subdirectories
if (!(std::filesystem::exists(dir, ec)))
{
BMCWEB_LOG_INFO << "Location " << dir << " nonexistent";
return files;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << dir
<< " existence: " << ec.message();
return files;
}
// Old-style C++ iter loop, to get error checking, not using ranged for
for (auto entries = std::filesystem::directory_iterator{dir};
entries != std::filesystem::end(entries); entries.increment(ec))
{
if (ec)
{
BMCWEB_LOG_ERROR << "Problem with " << dir
<< " iterating: " << ec.message();
break;
}
const auto& entry = *entries;
// Only match directories
if (!(entry.is_directory()))
{
continue;
}
auto dirBasename = entry.path().filename();
// Validating against denyList not needed for entry, only for instance
if (!(validateFilename(dirBasename)))
{
continue;
}
// Safe to use / operator here, jsonFilename constant always relative
auto jsonWithin = entry.path() / jsonFilename;
// The directory must contain the special JSON filename
if (!(std::filesystem::exists(jsonWithin, ec)))
{
continue;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << jsonWithin
<< " existence: " << ec.message();
continue;
}
files.emplace_back(dirBasename);
}
return files;
}
// Returns all existing instances under this hook, as a list of basenames
std::vector<std::string> Hook::listInstances() const
{
auto instanceDirs = listJsonDirs(locToFileDir(locBase()));
std::vector<std::string> result;
for (const auto& instanceDir : instanceDirs)
{
if (!(validateFilename(instanceDir, denyList)))
{
continue;
}
result.emplace_back(instanceDir.string());
}
return result;
}
void Hook::handleGetInstance(
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& idInstance)
{
// If optional middle keyword not in use, instance same as middle
if (pathMiddle.empty())
{
return handleGetMiddle(asyncResp, idInstance, pathMiddle);
}
// Before doing anything with filesystem, validate naming restrictions
if (!(validateFilename(idInstance, denyList)))
{
BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable";
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto outerUrl = locInstance(idInstance);
auto outerFilename = locToFileJson(outerUrl);
std::error_code ec;
BMCWEB_LOG_DEBUG << "Get instance " << idInstance << " checking "
<< outerFilename;
if (!(std::filesystem::exists(outerFilename, ec)))
{
BMCWEB_LOG_ERROR << "Instance not found with ID " << idInstance;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << outerFilename
<< " existence: " << ec.message();
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto contentOpt = readJsonFile(outerFilename);
if (!(contentOpt.has_value()))
{
BMCWEB_LOG_ERROR << "Problem reading file " << outerFilename;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto& content = *contentOpt;
// Regenerate these, as they were intentionally trimmed before storage
content["Id"] = idInstance;
content["@odata.id"] = outerUrl;
auto innerUrl = locMiddle(idInstance);
// Synthesize a correct link to middle layer
auto middleObject = nlohmann::json::object();
middleObject["@odata.id"] = innerUrl;
content[pathMiddle] = middleObject;
redfish::messages::success(asyncResp->res);
asyncResp->res.jsonValue = std::move(content);
}
void Hook::handleGetMiddle(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& idInstance,
const std::string& keywordMiddle)
{
// Before doing anything with filesystem, validate naming restrictions
if (!(validateFilename(idInstance, denyList)))
{
BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable";
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
// Validate the middle path component in URL is the expected constant
if (keywordMiddle != pathMiddle)
{
BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto innerUrl = locMiddle(idInstance);
auto innerDir = locToFileDir(innerUrl);
auto innerFilename = locToFileJson(innerUrl);
std::error_code ec;
BMCWEB_LOG_DEBUG << "Get middle " << idInstance
<< (keywordMiddle.empty() ? "" : "/") << keywordMiddle
<< " checking " << innerFilename;
if (!(std::filesystem::exists(innerFilename, ec)))
{
BMCWEB_LOG_ERROR << "Instance not found with ID " << idInstance;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << idInstance
<< " existence: " << ec.message();
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto contentOpt = readJsonFile(innerFilename);
if (!(contentOpt.has_value()))
{
BMCWEB_LOG_ERROR << "Problem reading file " << innerFilename;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto& content = *contentOpt;
// Regenerate these, as they were intentionally trimmed before storage
content["Id"] = pathMiddle;
content["@odata.id"] = innerUrl;
// Do not pass denylist in here, it is only for instance, not entry
auto files = listJsonDirs(innerDir);
// Synthesize special "Members" array with links to all our entries
auto membersArray = nlohmann::json::array();
for (const auto& file : files)
{
// Safe to use / operator here, "file" already known to be relative
std::filesystem::path entryUrl = innerUrl / file;
auto fileObject = nlohmann::json::object();
fileObject["@odata.id"] = entryUrl;
fileObject["Id"] = file;
// Automatically expand only the fields listed in expandList
if (!(expandList.empty()))
{
std::filesystem::path entryFilename = locToFileJson(entryUrl);
auto entryContentOpt = readJsonFile(entryFilename);
if (entryContentOpt.has_value())
{
auto entryContent = *entryContentOpt;
for (const auto& key : expandList)
{
auto valueIter = entryContent.find(key);
if (valueIter != entryContent.end())
{
fileObject[key] = *valueIter;
}
}
}
}
membersArray += fileObject;
}
// Finish putting the pieces together
content["Members"] = membersArray;
content["Members@odata.count"] = files.size();
redfish::messages::success(asyncResp->res);
asyncResp->res.jsonValue = std::move(content);
}
void Hook::handleGetEntry(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const nlohmann::json::json_pointer& jsonPtr,
const std::string& idInstance,
const std::string& keywordMiddle,
const std::string& idEntry)
{
// Before doing anything with filesystem, validate naming restrictions
if (!(validateFilename(idInstance, denyList)))
{
BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable";
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
// Validate the middle path component in URL is the expected constant
if (keywordMiddle != pathMiddle)
{
BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
// Unlike instance, names on denyList are perfectly OK at this layer
if (!(validateFilename(idEntry)))
{
BMCWEB_LOG_ERROR << "Entry ID within URL not acceptable";
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto entryUrl = locEntry(idInstance, idEntry);
auto entryFilename = locToFileJson(entryUrl);
std::error_code ec;
BMCWEB_LOG_DEBUG << "Get entry " << idInstance
<< (keywordMiddle.empty() ? "" : "/") << keywordMiddle
<< "/" << idEntry << " checking " << entryFilename;
const auto doesExist = (std::filesystem::exists(entryFilename, ec));
if (!doesExist)
{
BMCWEB_LOG_ERROR << "Entry not found with ID " << idEntry;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
if (ec)
{
BMCWEB_LOG_ERROR << "Problem checking for " << idEntry
<< " existence: " << ec.message();
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto contentOpt = readJsonFile(entryFilename);
if (!(contentOpt.has_value()))
{
BMCWEB_LOG_ERROR << "Problem reading file " << entryFilename;
asyncResp->res.result(boost::beast::http::status::not_found);
return;
}
auto& content = *contentOpt;
// Regenerate these, as they were intentionally trimmed before storage
content["Id"] = idEntry;
content["@odata.id"] = entryUrl;
redfish::messages::success(asyncResp->res);
asyncResp->res.jsonValue[jsonPtr] = std::move(content);
}
void Hook::deleteAll()
{
std::error_code ec;
auto count = std::filesystem::remove_all(pathPrefix, ec);
if (ec)
{
BMCWEB_LOG_ERROR << "Problem with " << pathPrefix
<< " deleting: " << ec.message();
}
if (count > 0)
{
BMCWEB_LOG_INFO << "Deleted all " << count << " files/dirs from "
<< pathPrefix;
}
}
void Hook::setPathPrefix(const std::filesystem::path& newPrefix)
{
// This function is only for testing, loudly warn if used
BMCWEB_LOG_WARNING << "Changing path prefix to " << newPrefix;
pathPrefix = newPrefix;
}
// Constructs a hook with known-good settings for usage with LogServices
Hook makeLogServices()
{
const std::string pathBase{"Systems/system/LogServices"};
const std::string midWord{"Entries"};
// These names come from requestRoutesSystemLogServiceCollection()
std::vector<std::string> denyList{"EventLog", "Dump", "Crashdump",
"HostLogger"};
// These names come from the "required" field of LogEntry JSON schema
std::vector<std::string> expandList{"EntryType", "@odata.id", "@odata.type",
"Id", "Name"};
// Additional useful names to pre-expand
expandList.emplace_back("Created");
return {pathBase, midWord, denyList, expandList};
}
std::shared_ptr<Hook>
rememberLogServices(const std::shared_ptr<Hook>& hookIncoming)
{
static std::shared_ptr<Hook> hookLogServices = nullptr;
// If incoming pointer is valid, remember it for next time
if (hookIncoming)
{
hookLogServices = hookIncoming;
}
return hookLogServices;
}
} // namespace external_storer
namespace redfish
{
inline void handleLogServiceInstancePost(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleCreateInstance(req, asyncResp);
}
inline void handleLogServiceMiddlePost(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName, const std::string& instance)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleCreateMiddle(req, asyncResp, instance);
}
inline void handleLogServiceEntryPost(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName, const std::string& instance,
const std::string& middle)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleCreateEntry(req, asyncResp, instance, middle);
}
inline void handleLogServiceInstanceGet(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName, const std::string& instance)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleGetInstance(asyncResp, instance);
}
inline void handleLogServiceMiddleGet(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName, const std::string& instance,
const std::string& middle)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleGetMiddle(asyncResp, instance, middle);
}
inline void handleLogServiceEntryGet(
App& app, const std::shared_ptr<external_storer::Hook>& hook,
const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
const std::string& systemName, const std::string& instance,
const std::string& middle, const std::string& entry)
{
if (!redfish::setUpRedfishRoute(app, req, asyncResp))
{
return;
}
if (systemName != "system")
{
messages::resourceNotFound(asyncResp->res, "ComputerSystem",
systemName);
return;
}
hook->handleGetEntry(asyncResp, ""_json_pointer, instance, middle, entry);
}
void requestRoutesExternalStorerLogServices(
App& app, const std::shared_ptr<external_storer::Hook>& hook)
{
// Only 0-argument, 1-argument, and 2-argument POST routes exist
// There intentionally is no 3-argument POST handler
BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/")
.privileges(redfish::privileges::postLogService)
.methods(boost::beast::http::verb::post)(
std::bind_front(handleLogServiceInstancePost, std::ref(app), hook));
BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/")
.privileges(redfish::privileges::postLogService)
.methods(boost::beast::http::verb::post)(
std::bind_front(handleLogServiceMiddlePost, std::ref(app), hook));
BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/<str>/")
.privileges(redfish::privileges::postLogService)
.methods(boost::beast::http::verb::post)(
std::bind_front(handleLogServiceEntryPost, std::ref(app), hook));
// Only 1-argument, 2-argument, and 3-argument GET routes are here
// The 0-argument GET route is already handled by the integration point
// It is at log_services.hpp requestRoutesSystemLogServiceCollection()
BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/")
.privileges(redfish::privileges::getLogService)
.methods(boost::beast::http::verb::get)(
std::bind_front(handleLogServiceInstanceGet, std::ref(app), hook));
BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/<str>/")
.privileges(redfish::privileges::getLogService)
.methods(boost::beast::http::verb::get)(
std::bind_front(handleLogServiceMiddleGet, std::ref(app), hook));
BMCWEB_ROUTE(app,
"/redfish/v1/Systems/<str>/LogServices/<str>/<str>/<str>/")
.privileges(redfish::privileges::getLogService)
.methods(boost::beast::http::verb::get)(
std::bind_front(handleLogServiceEntryGet, std::ref(app), hook));
// The integration point also needs to access the correct hook
external_storer::rememberLogServices(hook);
}
void requestRoutesExternalStorer(App& app)
{
auto hookLogServices = std::make_shared<external_storer::Hook>(
external_storer::makeLogServices());
// The shared_ptr will be copied, stretching out its lifetime
requestRoutesExternalStorerLogServices(app, hookLogServices);
}
} // namespace redfish