| #pragma once |
| |
| // ExternalStorer allows external users (HTTP clients) to temporarily store |
| // their data on this Redfish server, hence its name. |
| |
| // The intended use cases are for catching logging messages and hardware error |
| // notifications from the host, but ExternalStorer is not limited to these. |
| // The backing store for this data is a RAM disk (tmpfs), so it will be lost |
| // when the BMC reboots or powers down. Not intended for long-term storage. |
| |
| // To comply with relevant Redfish schemas, ExternalStorer will carefully |
| // merge user-provided data with what is already on the system. Overwriting |
| // system-provided data is not allowed. There are 3 addressing levels: |
| |
| // Hook = The integration point, an existing Redfish URL containing one |
| // writable collection, as allowed by a schema. |
| // Instance = An external user will POST to create a new collection here, |
| // which will be added to what is already visible at the Hook. |
| // Entry = An external user will POST to create a new entry here, |
| // which will be added to that collection (the Instance). |
| |
| // Example usage: |
| // GET /redfish/v1/Systems/system/LogServices |
| // -> This is a hook, it contains a writable "Members" collection |
| // POST /redfish/v1/Systems/system/LogServices |
| // -> Created instance, let's assume BMC assigns the ID of "MY_LOG" |
| // GET /redfish/v1/Systems/system/LogServices/MY_LOG |
| // -> As per schema, this contains an additional path component "Entries" |
| // GET /redfish/v1/Systems/system/LogServices/MY_LOG/Entries |
| // -> This is a container, ready to go, contains empty "Members" collection |
| // POST /redfish/v1/Systems/system/LogServices/MY_LOG/Entries |
| // -> Created entry, let's assume BMC assigns the ID of "MY_ALERT" |
| // GET /redfish/v1/Systems/system/LogServices/MY_LOG/Entries/MY_ALERT |
| // -> This retrieves the data previously stored when creating that entry |
| // GET /redfish/v1/Systems/system/LogServices/MY_LOG/Entries |
| // -> This container is no longer empty, it now has one element |
| // GET /redfish/v1/Systems/system/LogServices/MY_LOG |
| // -> Retrieves some content previously stored when creating the instance |
| // GET /redfish/v1/Systems/system/LogServices |
| // -> The "Members" collection now contains "MY_LOG" in addition to before |
| |
| // All data is expressed in the form of a JSON dictionary. The backing store |
| // uses a similar directory layout, including the extra "Entries" |
| // subdirectory. The JSON content for the collection itself is stored as two |
| // special-case "index.json" filenames within that collection's directories. |
| // The on-disk file format is whatever is provided by the defaults for the |
| // nlohmann::json::dump() and nlohmann::json::parse() functions. |
| |
| // Filesystem layout: |
| // Directory /run/bmcweb/redfish/v1/HOOK/INSTANCE |
| // Directory /run/bmcweb/redfish/v1/HOOK/INSTANCE/MIDDLE |
| // Directory /run/bmcweb/redfish/v1/HOOK/INSTANCE/MIDDLE/ENTRY |
| // File /run/bmcweb/redfish/v1/HOOK/INSTANCE/index.json |
| // File /run/bmcweb/redfish/v1/HOOK/INSTANCE/MIDDLE/index.json |
| // File /run/bmcweb/redfish/v1/HOOK/INSTANCE/MIDDLE/ENTRY/index.json |
| |
| // HOOK is hardcoded, trimmed from URL, example: "Systems/system/LogServices" |
| // MIDDLE is hardcoded, a single word, example: "Entries" |
| // INSTANCE and ENTRY are user-generated (within reason) or BMC-generated |
| // Each ENTRY index.json file contains JSON of one entry within an instance |
| // The two higher "index.json" files contain JSON of that instance itself |
| |
| // SECURITY WARNING: There is currently no limit on the amount of storage |
| // taken, nor any automatic cleanup of old content, so clients can cause |
| // a denial of service attack by consuming all storage. This will be |
| // addressed by future work. |
| |
| #include "app.hpp" |
| |
| #include <boost/uuid/uuid.hpp> |
| #include <boost/uuid/uuid_generators.hpp> |
| #include <boost/uuid/uuid_io.hpp> |
| |
| #include <cmath> |
| #include <cstddef> |
| #include <fstream> |
| #include <sstream> |
| |
| namespace external_storer |
| { |
| |
| // These should become constexpr in a future compiler |
| inline const std::filesystem::path defPathPrefix{"/run/bmcweb"}; |
| inline const std::filesystem::path redfishPrefix{"/redfish/v1"}; |
| inline const std::filesystem::path jsonFilename{"index.json"}; |
| |
| // Maximum allowed file size, helps to avoid DoS attacks |
| inline const size_t maxFileSize{static_cast<size_t>(1024 * 1024)}; |
| |
| // This class only holds configuration and accounting data for the hook. |
| // As for user-provided data, it is intentionally not here, as it is always |
| // fetched from the filesystem backing store when needed. |
| class Hook |
| { |
| private: |
| // Relative location of hook, after redfishPrefix, before instance |
| std::filesystem::path pathBase; |
| |
| // Optional middle keyword, required by some schemas, example "Entries" |
| std::filesystem::path pathMiddle; |
| |
| // Disallow these user instances, avoid already-existing path components |
| std::vector<std::string> denyList; |
| |
| // Automatically expand these fields when listing the array of entries |
| std::vector<std::string> expandList; |
| |
| // The root directory for local storage, changeable only for testing |
| std::filesystem::path pathPrefix; |
| |
| public: |
| Hook(const std::string& b, const std::string& m, |
| const std::vector<std::string>& d, const std::vector<std::string>& e) : |
| pathBase(b), |
| pathMiddle(m), denyList(d), expandList(e), pathPrefix(defPathPrefix) |
| {} |
| |
| void handleCreateInstance( |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp); |
| void handleCreateMiddle(const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& instance); |
| void handleCreateEntry(const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& instance, |
| const std::string& middle); |
| |
| // The 0-argument Get handled by just-in-time insert at integration point |
| void handleGetInstance(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& instance); |
| void handleGetMiddle(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& instance, |
| const std::string& middle); |
| void handleGetEntry(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const nlohmann::json::json_pointer& jsonPtr, |
| const std::string& instance, const std::string& middle, |
| const std::string& entry); |
| |
| // For use by the integration point |
| std::vector<std::string> listInstances() const; |
| |
| // Utility functions for building up locations |
| std::filesystem::path locBase() const; |
| std::filesystem::path locInstance(const std::string& instance) const; |
| std::filesystem::path locMiddle(const std::string& instance) const; |
| std::filesystem::path locEntry(const std::string& instance, |
| const std::string& entry) const; |
| std::filesystem::path locToFileDir(const std::filesystem::path& loc) const; |
| std::filesystem::path locToFileJson(const std::filesystem::path& loc) const; |
| |
| // For use only during testing |
| void deleteAll(); |
| void setPathPrefix(const std::filesystem::path& newPrefix); |
| }; |
| |
| Hook makeLogServices(); |
| std::shared_ptr<Hook> |
| rememberLogServices(const std::shared_ptr<Hook>& hookIncoming = nullptr); |
| |
| } // namespace external_storer |
| |
| namespace redfish |
| { |
| |
| // The URL layout under LogServices requires "Entries" path component, |
| // which seems unnecessary, but is required by the schema. |
| // POST(HOOK) = create new instance |
| // POST(HOOK/INSTANCE) = create new entry | these 2 endpoints |
| // POST(HOOK/INSTANCE/Entries) = create new entry | do the same thing |
| // POST(HOOK/INSTANCE/Entries/ENTRY) = not allowed |
| // GET(HOOK) = supplement existing hook with our added instances |
| // GET(HOOK/INSTANCE) = return boilerplate of desired instance |
| // GET(HOOK/INSTANCE/Entries) = return Members array of all entries |
| // GET(HOOK/INSTANCE/Entries/ENTRY) = return content of desired entry |
| void requestRoutesExternalStorerLogServices( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook); |
| |
| // NOTE: Currently, this works, but by luck, perhaps due to the fact that |
| // the ExternalStorer routes are requested last. The router currently does |
| // not cleanly support overlapping routes. So, the wildcard GET matchers |
| // currently can not cleanly coexist with the various hardcoded string |
| // matchers (see denyList) at the same URL position. One possible |
| // solution is to add a priority system to disambiguate, as discussed here: |
| // https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/43502 |
| void requestRoutesExternalStorer(App& app); |
| |
| } // namespace redfish |