/* // Copyright (c) 2018 Intel 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. */ #pragma once #include "account_service.hpp" #include "app.hpp" #include "async_resp.hpp" #include "credential_pipe.hpp" #include "dbus_utility.hpp" #include "generated/enums/virtual_media.hpp" #include "query.hpp" #include "registries/privilege_registry.hpp" #include "utils/json_utils.hpp" #include #include #include #include #include #include namespace redfish { enum class VmMode { Invalid, Legacy, Proxy }; inline VmMode parseObjectPathAndGetMode(const sdbusplus::message::object_path& itemPath, const std::string& resName) { std::string thisPath = itemPath.filename(); BMCWEB_LOG_DEBUG("Filename: {}, ThisPath: {}", itemPath.str, thisPath); if (thisPath.empty()) { return VmMode::Invalid; } if (thisPath != resName) { return VmMode::Invalid; } auto mode = itemPath.parent_path(); auto type = mode.parent_path(); if (mode.filename().empty() || type.filename().empty()) { return VmMode::Invalid; } if (type.filename() != "VirtualMedia") { return VmMode::Invalid; } std::string modeStr = mode.filename(); if (modeStr == "Legacy") { return VmMode::Legacy; } if (modeStr == "Proxy") { return VmMode::Proxy; } return VmMode::Invalid; } using CheckItemHandler = std::function&, const std::pair&)>; inline void findAndParseObject(const std::string& service, const std::string& resName, const std::shared_ptr& asyncResp, CheckItemHandler&& handler) { sdbusplus::message::object_path path("/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [service, resName, asyncResp, handler](const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_DEBUG("DBUS response error"); return; } for (const auto& item : subtree) { VmMode mode = parseObjectPathAndGetMode(item.first, resName); if (mode != VmMode::Invalid) { handler(service, resName, asyncResp, item); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); asyncResp->res.result(boost::beast::http::status::not_found); }); } /** * @brief Function extracts transfer protocol name from URI. */ inline std::string getTransferProtocolTypeFromUri(const std::string& imageUri) { boost::system::result url = boost::urls::parse_uri(imageUri); if (!url) { return "None"; } std::string_view scheme = url->scheme(); if (scheme == "smb") { return "CIFS"; } if (scheme == "https") { return "HTTPS"; } return "None"; } /** * @brief Read all known properties from VM object interfaces */ inline void vmParseInterfaceObject(const dbus::utility::DBusInterfacesMap& interfaces, const std::shared_ptr& asyncResp) { for (const auto& [interface, values] : interfaces) { if (interface == "xyz.openbmc_project.VirtualMedia.MountPoint") { for (const auto& [property, value] : values) { if (property == "EndpointId") { const std::string* endpointIdValue = std::get_if(&value); if (endpointIdValue == nullptr) { continue; } if (!endpointIdValue->empty()) { // Proxy mode asyncResp->res .jsonValue["Oem"]["OpenBMC"]["WebSocketEndpoint"] = *endpointIdValue; asyncResp->res.jsonValue["TransferProtocolType"] = "OEM"; } } if (property == "ImageURL") { const std::string* imageUrlValue = std::get_if(&value); if (imageUrlValue != nullptr && !imageUrlValue->empty()) { std::filesystem::path filePath = *imageUrlValue; if (!filePath.has_filename()) { // this will handle https share, which not // necessarily has to have filename given. asyncResp->res.jsonValue["ImageName"] = ""; } else { asyncResp->res.jsonValue["ImageName"] = filePath.filename(); } asyncResp->res.jsonValue["Image"] = *imageUrlValue; asyncResp->res.jsonValue["TransferProtocolType"] = getTransferProtocolTypeFromUri(*imageUrlValue); asyncResp->res.jsonValue["ConnectedVia"] = virtual_media::ConnectedVia::URI; } } if (property == "WriteProtected") { const bool* writeProtectedValue = std::get_if(&value); if (writeProtectedValue != nullptr) { asyncResp->res.jsonValue["WriteProtected"] = *writeProtectedValue; } } } } if (interface == "xyz.openbmc_project.VirtualMedia.Process") { for (const auto& [property, value] : values) { if (property == "Active") { const bool* activeValue = std::get_if(&value); if (activeValue == nullptr) { BMCWEB_LOG_DEBUG("Value Active not found"); return; } asyncResp->res.jsonValue["Inserted"] = *activeValue; if (*activeValue) { asyncResp->res.jsonValue["ConnectedVia"] = virtual_media::ConnectedVia::Applet; } } } } } } /** * @brief Fill template for Virtual Media Item. */ inline nlohmann::json vmItemTemplate(const std::string& name, const std::string& resName) { nlohmann::json item; item["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}", name, resName); item["@odata.type"] = "#VirtualMedia.v1_3_0.VirtualMedia"; item["Name"] = "Virtual Removable Media"; item["Id"] = resName; item["WriteProtected"] = true; item["ConnectedVia"] = virtual_media::ConnectedVia::NotConnected; item["MediaTypes"] = nlohmann::json::array_t({"CD", "USBStick"}); item["TransferMethod"] = "Stream"; item["Oem"]["OpenBMC"]["@odata.type"] = "#OemVirtualMedia.v1_0_0.VirtualMedia"; item["Oem"]["OpenBMC"]["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}#/Oem/OpenBMC", name, resName); return item; } /** * @brief Fills collection data */ inline void getVmResourceList(std::shared_ptr asyncResp, const std::string& service, const std::string& name) { BMCWEB_LOG_DEBUG("Get available Virtual Media resources."); sdbusplus::message::object_path objPath( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, objPath, [name, asyncResp{std::move(asyncResp)}]( const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_DEBUG("DBUS response error"); return; } nlohmann::json& members = asyncResp->res.jsonValue["Members"]; members = nlohmann::json::array(); for (const auto& object : subtree) { nlohmann::json item; std::string path = object.first.filename(); if (path.empty()) { continue; } item["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}", name, path); members.emplace_back(std::move(item)); } asyncResp->res.jsonValue["Members@odata.count"] = members.size(); }); } inline void afterGetVmData(const std::string& name, const std::string& /*service*/, const std::string& resName, const std::shared_ptr& asyncResp, const std::pair& item) { VmMode mode = parseObjectPathAndGetMode(item.first, resName); if (mode == VmMode::Invalid) { return; } asyncResp->res.jsonValue = vmItemTemplate(name, resName); // Check if dbus path is Legacy type if (mode == VmMode::Legacy) { asyncResp->res.jsonValue["Actions"]["#VirtualMedia.InsertMedia"] ["target"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.InsertMedia", name, resName); } vmParseInterfaceObject(item.second, asyncResp); asyncResp->res.jsonValue["Actions"]["#VirtualMedia.EjectMedia"] ["target"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.EjectMedia", name, resName); } /** * @brief Fills data for specific resource */ inline void getVmData(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, const std::string& resName) { BMCWEB_LOG_DEBUG("Get Virtual Media resource data."); findAndParseObject(service, resName, asyncResp, std::bind_front(afterGetVmData, name)); } /** * @brief Transfer protocols supported for InsertMedia action. * */ enum class TransferProtocol { https, smb, invalid }; /** * @brief Function extracts transfer protocol type from URI. * */ inline std::optional getTransferProtocolFromUri(boost::urls::url_view imageUri) { std::string_view scheme = imageUri.scheme(); if (scheme == "smb") { return TransferProtocol::smb; } if (scheme == "https") { return TransferProtocol::https; } if (!scheme.empty()) { return TransferProtocol::invalid; } return {}; } /** * @brief Function convert transfer protocol from string param. * */ inline std::optional getTransferProtocolFromParam( const std::optional& transferProtocolType) { if (!transferProtocolType) { return {}; } if (*transferProtocolType == "CIFS") { return TransferProtocol::smb; } if (*transferProtocolType == "HTTPS") { return TransferProtocol::https; } return TransferProtocol::invalid; } /** * @brief Function extends URI with transfer protocol type. * */ inline std::string getUriWithTransferProtocol(const std::string& imageUri, const TransferProtocol& transferProtocol) { if (transferProtocol == TransferProtocol::smb) { return "smb://" + imageUri; } if (transferProtocol == TransferProtocol::https) { return "https://" + imageUri; } return imageUri; } struct InsertMediaActionParams { std::optional imageUrl; std::optional userName; std::optional password; std::optional transferMethod; std::optional transferProtocolType; std::optional writeProtected = true; std::optional inserted; }; /** * @brief Function transceives data with dbus directly. * * All BMC state properties will be retrieved before sending reset request. */ inline void doMountVmLegacy(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, const std::string& imageUrl, bool rw, std::string&& userName, std::string&& password) { int fd = -1; std::shared_ptr secretPipe; if (!userName.empty() || !password.empty()) { // Payload must contain data + NULL delimiters constexpr const size_t secretLimit = 1024; if (userName.size() + password.size() + 2 > secretLimit) { BMCWEB_LOG_ERROR("Credentials too long to handle"); messages::unrecognizedRequestBody(asyncResp->res); return; } // Open pipe secretPipe = std::make_shared( crow::connections::systemBus->get_io_context()); fd = secretPipe->fd(); // Pass secret over pipe secretPipe->asyncWrite( std::move(userName), std::move(password), [asyncResp, secretPipe](const boost::system::error_code& ec, std::size_t) { if (ec) { BMCWEB_LOG_ERROR("Failed to pass secret: {}", ec); messages::internalError(asyncResp->res); } }); } dbus::utility::DbusVariantType unixFd( std::in_place_type, fd); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia/Legacy"); path /= name; crow::connections::systemBus->async_method_call( [asyncResp, secretPipe](const boost::system::error_code& ec, bool success) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } if (!success) { BMCWEB_LOG_ERROR("Service responded with error"); messages::internalError(asyncResp->res); } }, service, path.str, "xyz.openbmc_project.VirtualMedia.Legacy", "Mount", imageUrl, rw, unixFd); } /** * @brief Function validate parameters of insert media request. * */ inline void validateParams(const std::shared_ptr& asyncResp, const std::string& service, const std::string& resName, InsertMediaActionParams& actionParams) { BMCWEB_LOG_DEBUG("Validation started"); // required param imageUrl must not be empty if (!actionParams.imageUrl) { BMCWEB_LOG_ERROR("Request action parameter Image is empty."); messages::propertyValueFormatError(asyncResp->res, "", "Image"); return; } // optional param inserted must be true if (actionParams.inserted && !*actionParams.inserted) { BMCWEB_LOG_ERROR( "Request action optional parameter Inserted must be true."); messages::actionParameterNotSupported(asyncResp->res, "Inserted", "InsertMedia"); return; } // optional param transferMethod must be stream if (actionParams.transferMethod && (*actionParams.transferMethod != "Stream")) { BMCWEB_LOG_ERROR("Request action optional parameter " "TransferMethod must be Stream."); messages::actionParameterNotSupported(asyncResp->res, "TransferMethod", "InsertMedia"); return; } boost::system::result url = boost::urls::parse_uri(*actionParams.imageUrl); if (!url) { messages::actionParameterValueFormatError( asyncResp->res, *actionParams.imageUrl, "Image", "InsertMedia"); return; } std::optional uriTransferProtocolType = getTransferProtocolFromUri(*url); std::optional paramTransferProtocolType = getTransferProtocolFromParam(actionParams.transferProtocolType); // ImageUrl does not contain valid protocol type if (uriTransferProtocolType && *uriTransferProtocolType == TransferProtocol::invalid) { BMCWEB_LOG_ERROR("Request action parameter ImageUrl must " "contain specified protocol type from list: " "(smb, https)."); messages::resourceAtUriInUnknownFormat(asyncResp->res, *url); return; } // transferProtocolType should contain value from list if (paramTransferProtocolType && *paramTransferProtocolType == TransferProtocol::invalid) { BMCWEB_LOG_ERROR("Request action parameter TransferProtocolType " "must be provided with value from list: " "(CIFS, HTTPS)."); messages::propertyValueNotInList( asyncResp->res, actionParams.transferProtocolType.value_or(""), "TransferProtocolType"); return; } // valid transfer protocol not provided either with URI nor param if (!uriTransferProtocolType && !paramTransferProtocolType) { BMCWEB_LOG_ERROR("Request action parameter ImageUrl must " "contain specified protocol type or param " "TransferProtocolType must be provided."); messages::resourceAtUriInUnknownFormat(asyncResp->res, *url); return; } // valid transfer protocol provided both with URI and param if (paramTransferProtocolType && uriTransferProtocolType) { // check if protocol is the same for URI and param if (*paramTransferProtocolType != *uriTransferProtocolType) { BMCWEB_LOG_ERROR("Request action parameter " "TransferProtocolType must contain the " "same protocol type as protocol type " "provided with param imageUrl."); messages::actionParameterValueTypeError( asyncResp->res, actionParams.transferProtocolType.value_or(""), "TransferProtocolType", "InsertMedia"); return; } } // validation passed, add protocol to URI if needed if (!uriTransferProtocolType && paramTransferProtocolType) { actionParams.imageUrl = getUriWithTransferProtocol( *actionParams.imageUrl, *paramTransferProtocolType); } if (!actionParams.userName) { actionParams.userName = ""; } if (!actionParams.password) { actionParams.password = ""; } doMountVmLegacy(asyncResp, service, resName, *actionParams.imageUrl, !(actionParams.writeProtected.value_or(false)), std::move(*actionParams.userName), std::move(*actionParams.password)); } /** * @brief Function transceives data with dbus directly. * * All BMC state properties will be retrieved before sending reset request. */ inline void doEjectAction(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, bool legacy) { // Legacy mount requires parameter with image if (legacy) { crow::connections::systemBus->async_method_call( [asyncResp](const boost::system::error_code& ec) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } }, service, "/xyz/openbmc_project/VirtualMedia/Legacy/" + name, "xyz.openbmc_project.VirtualMedia.Legacy", "Unmount"); } else // proxy { crow::connections::systemBus->async_method_call( [asyncResp](const boost::system::error_code& ec) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } }, service, "/xyz/openbmc_project/VirtualMedia/Proxy/" + name, "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); } } inline void handleManagersVirtualMediaActionInsertPost( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } constexpr std::string_view action = "VirtualMedia.InsertMedia"; if (name != "bmc") { messages::resourceNotFound(asyncResp->res, action, resName); return; } InsertMediaActionParams actionParams; // Read obligatory parameters (url of image) if (!json_util::readJsonAction( req, asyncResp->res, "Image", actionParams.imageUrl, "WriteProtected", actionParams.writeProtected, "UserName", actionParams.userName, "Password", actionParams.password, "Inserted", actionParams.inserted, "TransferMethod", actionParams.transferMethod, "TransferProtocolType", actionParams.transferProtocolType)) { return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, action, actionParams, resName](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) mutable { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::resourceNotFound(asyncResp->res, action, resName); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [service, resName, action, actionParams, asyncResp]( const boost::system::error_code& ec2, const dbus::utility::ManagedObjectType& subtree) mutable { if (ec2) { // Not possible in proxy mode BMCWEB_LOG_DEBUG("InsertMedia not " "allowed in proxy mode"); messages::resourceNotFound(asyncResp->res, action, resName); return; } for (const auto& object : subtree) { VmMode mode = parseObjectPathAndGetMode(object.first, resName); if (mode == VmMode::Legacy) { validateParams(asyncResp, service, resName, actionParams); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); }); }); } inline void handleManagersVirtualMediaActionEject( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& managerName, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } constexpr std::string_view action = "VirtualMedia.EjectMedia"; if (managerName != "bmc") { messages::resourceNotFound(asyncResp->res, action, resName); return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, action, resName](const boost::system::error_code& ec2, const dbus::utility::MapperGetObject& getObjectType) { if (ec2) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec2); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [resName, service, action, asyncResp](const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper : No Service found"); messages::resourceNotFound(asyncResp->res, action, resName); return; } for (const auto& object : subtree) { VmMode mode = parseObjectPathAndGetMode(object.first, resName); if (mode != VmMode::Invalid) { doEjectAction(asyncResp, service, resName, mode == VmMode::Legacy); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); }); }); } inline void handleManagersVirtualMediaCollectionGet( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (name != "bmc") { messages::resourceNotFound(asyncResp->res, "VirtualMedia", name); return; } asyncResp->res.jsonValue["@odata.type"] = "#VirtualMediaCollection.VirtualMediaCollection"; asyncResp->res.jsonValue["Name"] = "Virtual Media Services"; asyncResp->res.jsonValue["@odata.id"] = boost::urls::format("/redfish/v1/Managers/{}/VirtualMedia", name); dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, name](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); getVmResourceList(asyncResp, service, name); }); } inline void handleVirtualMediaGet(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (name != "bmc") { messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, name, resName](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); getVmData(asyncResp, service, name, resName); }); } inline void requestNBDVirtualMediaRoutes(App& app) { BMCWEB_ROUTE( app, "/redfish/v1/Managers//VirtualMedia//Actions/VirtualMedia.InsertMedia") .privileges(redfish::privileges::postVirtualMedia) .methods(boost::beast::http::verb::post)(std::bind_front( handleManagersVirtualMediaActionInsertPost, std::ref(app))); BMCWEB_ROUTE( app, "/redfish/v1/Managers//VirtualMedia//Actions/VirtualMedia.EjectMedia") .privileges(redfish::privileges::postVirtualMedia) .methods(boost::beast::http::verb::post)(std::bind_front( handleManagersVirtualMediaActionEject, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/Managers//VirtualMedia/") .privileges(redfish::privileges::getVirtualMediaCollection) .methods(boost::beast::http::verb::get)(std::bind_front( handleManagersVirtualMediaCollectionGet, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/Managers//VirtualMedia//") .privileges(redfish::privileges::getVirtualMedia) .methods(boost::beast::http::verb::get)( std::bind_front(handleVirtualMediaGet, std::ref(app))); } } // namespace redfish