summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--virtual-media/src/main.cpp18
-rw-r--r--virtual-media/src/state_machine.hpp766
2 files changed, 782 insertions, 2 deletions
diff --git a/virtual-media/src/main.cpp b/virtual-media/src/main.cpp
index 792cdf2..3636e59 100644
--- a/virtual-media/src/main.cpp
+++ b/virtual-media/src/main.cpp
@@ -1,5 +1,6 @@
#include "configuration.hpp"
#include "logger.hpp"
+#include "state_machine.hpp"
#include "system.hpp"
#include <sys/mount.h>
@@ -40,12 +41,25 @@ class App
objManager = std::make_shared<sdbusplus::server::manager::manager>(
*bus, "/xyz/openbmc_project/VirtualMedia");
- devMonitor.run([](const NBDDevice& device, StateChange change) {
- // placeholder for some future actions
+ for (const auto& [name, entry] : config.mountPoints)
+ {
+ mpsm[name] = std::make_shared<MountPointStateMachine>(
+ ioc, devMonitor, name, entry);
+ mpsm[name]->emitRegisterDBusEvent(bus, objServer);
+ }
+
+ devMonitor.run([this](const NBDDevice& device, StateChange change) {
+ for (auto& [name, entry] : mpsm)
+ {
+ entry->emitUdevStateChangeEvent(device, change);
+ }
});
}
private:
+ boost::container::flat_map<std::string,
+ std::shared_ptr<MountPointStateMachine>>
+ mpsm;
boost::asio::io_context& ioc;
std::shared_ptr<sdbusplus::asio::connection> bus;
std::shared_ptr<sdbusplus::asio::object_server> objServer;
diff --git a/virtual-media/src/state_machine.hpp b/virtual-media/src/state_machine.hpp
new file mode 100644
index 0000000..973d7fa
--- /dev/null
+++ b/virtual-media/src/state_machine.hpp
@@ -0,0 +1,766 @@
+#pragma once
+
+#include "configuration.hpp"
+#include "logger.hpp"
+#include "system.hpp"
+
+#include <sys/mount.h>
+
+#include <filesystem>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <sdbusplus/asio/object_server.hpp>
+#include <stdexcept>
+#include <variant>
+
+struct MountPointStateMachine
+{
+ struct InvalidStateError : std::runtime_error
+ {
+ InvalidStateError(const char* what) : std::runtime_error(what)
+ {
+ }
+ };
+
+ struct BasicState
+ {
+ BasicState(MountPointStateMachine& machine,
+ const char* stateName = nullptr) :
+ machine{machine},
+ stateName{stateName}
+ {
+ if (stateName != nullptr)
+ {
+ LogMsg(Logger::Debug, machine.name, " State changed to ",
+ stateName);
+ }
+ }
+
+ BasicState(const BasicState& state) :
+ machine{state.machine}, stateName{state.stateName}
+ {
+ }
+
+ BasicState(const BasicState& state, const char* stateName) :
+ machine{state.machine}, stateName{stateName}
+ {
+ LogMsg(Logger::Debug, machine.name, " State changed to ",
+ stateName);
+ }
+
+ BasicState& operator=(BasicState&& state)
+ {
+ machine = std::move(state.machine);
+ stateName = std::move(state.stateName);
+ return *this;
+ }
+
+ virtual void onEnter(){};
+
+ MountPointStateMachine& machine;
+ const char* stateName = nullptr;
+ };
+
+ struct InitialState : public BasicState
+ {
+ InitialState(const BasicState& state) :
+ BasicState(state, __FUNCTION__){};
+ InitialState(MountPointStateMachine& machine) :
+ BasicState(machine, __FUNCTION__){};
+ };
+
+ struct ReadyState : public BasicState
+ {
+ ReadyState(const BasicState& state) : BasicState(state, __FUNCTION__){};
+
+ virtual void onEnter()
+ {
+ if (machine.target)
+ {
+ machine.target.reset();
+ }
+ }
+ };
+
+ struct ActivatingState : public BasicState
+ {
+ ActivatingState(const BasicState& state) :
+ BasicState(state, __FUNCTION__)
+ {
+ }
+
+ virtual void onEnter()
+ {
+ machine.emitActivationStartedEvent();
+ }
+ };
+
+ struct WaitingForGadgetState : public BasicState
+ {
+ WaitingForGadgetState(const BasicState& state) :
+ BasicState(state, __FUNCTION__)
+ {
+ }
+
+ std::weak_ptr<Process> process;
+ };
+
+ struct ActiveState : public BasicState
+ {
+ ActiveState(const BasicState& state) : BasicState(state, __FUNCTION__)
+ {
+ }
+ ActiveState(const WaitingForGadgetState& state) :
+ BasicState(state, __FUNCTION__), process{state.process} {};
+
+ std::weak_ptr<Process> process;
+ };
+
+ struct WaitingForProcessEndState : public BasicState
+ {
+ WaitingForProcessEndState(const BasicState& state) :
+ BasicState(state, __FUNCTION__)
+ {
+ }
+ WaitingForProcessEndState(const ActiveState& state) :
+ BasicState(state, __FUNCTION__), process{state.process}
+ {
+ }
+ WaitingForProcessEndState(const WaitingForGadgetState& state) :
+ BasicState(state, __FUNCTION__), process{state.process}
+ {
+ }
+
+ std::weak_ptr<Process> process;
+ };
+
+ using State = std::variant<InitialState, ReadyState, ActivatingState,
+ WaitingForGadgetState, ActiveState,
+ WaitingForProcessEndState>;
+
+ struct BasicEvent
+ {
+ BasicEvent(const char* eventName) : eventName(eventName)
+ {
+ }
+
+ inline void transitionError(const char* en, const BasicState& state)
+ {
+ LogMsg(Logger::Critical, state.machine.name, " Unexpected event ",
+ eventName, " received in ", state.stateName,
+ "state. Review and correct state transisions.");
+ }
+ virtual State operator()(const InitialState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ virtual State operator()(const ReadyState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ virtual State operator()(const ActivatingState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ virtual State operator()(const WaitingForGadgetState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ virtual State operator()(const ActiveState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ virtual State operator()(const WaitingForProcessEndState& state)
+ {
+ transitionError(eventName, state);
+ return state;
+ }
+ const char* eventName;
+ };
+
+ struct RegisterDbusEvent : public BasicEvent
+ {
+ RegisterDbusEvent(
+ std::shared_ptr<sdbusplus::asio::connection> bus,
+ std::shared_ptr<sdbusplus::asio::object_server> objServer) :
+ BasicEvent(__FUNCTION__),
+ bus(bus), objServer(objServer),
+ emitMountEvent(std::move(emitMountEvent))
+ {
+ }
+
+ State operator()(const InitialState& state)
+ {
+ const bool isLegacy =
+ (state.machine.config.mode == Configuration::Mode::legacy);
+ addMountPointInterface(state);
+ addProcessInterface(state);
+ addServiceInterface(state, isLegacy);
+ return ReadyState(state);
+ }
+
+ template <typename AnyState>
+ State operator()(const AnyState& state)
+ {
+ LogMsg(Logger::Critical, state.machine.name,
+ " If you receiving this error, this means "
+ "your FSM is broken. Rethink!");
+ return InitialState(state);
+ }
+
+ private:
+ std::string getObjectPath(const MountPointStateMachine& machine)
+ {
+ LogMsg(Logger::Debug, "getObjectPath entry()");
+ std::string objPath;
+ if (machine.config.mode == Configuration::Mode::proxy)
+ {
+ objPath = "/xyz/openbmc_project/VirtualMedia/Proxy/";
+ }
+ else
+ {
+ objPath = "/xyz/openbmc_project/VirtualMedia/Legacy/";
+ }
+ return objPath;
+ }
+
+ std::string getObjectPath(const InitialState& state)
+ {
+ return getObjectPath(state.machine);
+ }
+
+ void addProcessInterface(const InitialState& state)
+ {
+ std::string objPath = getObjectPath(state);
+
+ auto processIface = objServer->add_interface(
+ objPath + state.machine.name,
+ "xyz.openbmc_project.VirtualMedia.Process");
+
+ processIface->register_property(
+ "Active", bool(false),
+ [](const bool& req, bool& property) { return 0; },
+ [& machine = state.machine](const bool& property) {
+ if (std::get_if<ActiveState>(&machine.state))
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ });
+ processIface->register_property(
+ "ExitCode", uint8_t(0),
+ [](const uint8_t& req, uint8_t& property) { return 0; },
+ [& machine = state.machine](const uint8_t& property) {
+ // TODO: indicate real value instead of success
+ return uint8_t(255);
+ });
+ processIface->initialize();
+ }
+
+ void addMountPointInterface(const InitialState& state)
+ {
+ std::string objPath = getObjectPath(state);
+
+ auto iface = objServer->add_interface(
+ objPath + state.machine.name,
+ "xyz.openbmc_project.VirtualMedia.MountPoint");
+ iface->register_property(
+ "Device", state.machine.config.nbdDevice.to_string());
+ iface->register_property("EndpointId",
+ state.machine.config.endPointId);
+ iface->register_property("Socket", state.machine.config.unixSocket);
+ iface->initialize();
+ }
+
+ void addServiceInterface(const InitialState& state, const bool isLegacy)
+ {
+ const std::string name = "xyz.openbmc_project.VirtualMedia." +
+ std::string(isLegacy ? "Legacy" : "Proxy");
+
+ const std::string path = getObjectPath(state) + state.machine.name;
+
+ auto iface = objServer->add_interface(path, name);
+
+ // Common unmount
+ iface->register_method(
+ "Unmount",
+ [& machine = state.machine](boost::asio::yield_context yield) {
+ LogMsg(Logger::Info, "[App]: Unmount called on ",
+ machine.name);
+ try
+ {
+ machine.emitUnmountEvent();
+ }
+ catch (InvalidStateError& e)
+ {
+ throw sdbusplus::exception::SdBusError(EPERM, e.what());
+ return false;
+ }
+
+ boost::asio::steady_timer timer(machine.ioc.get());
+ int waitCnt = 120;
+ while (waitCnt > 0)
+ {
+ if (std::get_if<ReadyState>(&machine.state))
+ {
+ break;
+ }
+ boost::system::error_code ignored_ec;
+ timer.expires_from_now(std::chrono::milliseconds(100));
+ timer.async_wait(yield[ignored_ec]);
+ waitCnt--;
+ }
+ return true;
+ });
+
+ // Common mount
+ const auto handleMount = [](boost::asio::yield_context yield,
+ MountPointStateMachine& machine) {
+ try
+ {
+ machine.emitMountEvent();
+ }
+ catch (InvalidStateError& e)
+ {
+ throw sdbusplus::exception::SdBusError(EPERM, e.what());
+ return false;
+ }
+
+ boost::asio::steady_timer timer(machine.ioc.get());
+ int waitCnt = 120;
+ while (waitCnt > 0)
+ {
+ if (std::get_if<ReadyState>(&machine.state))
+ {
+ return false;
+ }
+ if (std::get_if<ActiveState>(&machine.state))
+ {
+ return true;
+ }
+ boost::system::error_code ignored_ec;
+ timer.expires_from_now(std::chrono::milliseconds(100));
+ timer.async_wait(yield[ignored_ec]);
+ waitCnt--;
+ }
+ return false;
+ };
+
+ // Mount specialization
+ if (isLegacy)
+ {
+ iface->register_method(
+ "Mount", [& machine = state.machine, this,
+ handleMount](boost::asio::yield_context yield,
+ std::string imgUrl, bool rw) {
+ LogMsg(Logger::Info, "[App]: Mount called on ",
+ getObjectPath(machine), machine.name);
+
+ machine.target = {imgUrl};
+ return handleMount(yield, machine);
+ });
+ }
+ else
+ {
+ iface->register_method(
+ "Mount", [& machine = state.machine, this,
+ handleMount](boost::asio::yield_context yield) {
+ LogMsg(Logger::Info, "[App]: Mount called on ",
+ getObjectPath(machine), machine.name);
+
+ return handleMount(yield, machine);
+ });
+ }
+
+ iface->initialize();
+ }
+
+ std::shared_ptr<sdbusplus::asio::connection> bus;
+ std::shared_ptr<sdbusplus::asio::object_server> objServer;
+ std::function<void(void)> emitMountEvent;
+ };
+
+ struct MountEvent : public BasicEvent
+ {
+ MountEvent() : BasicEvent(__FUNCTION__)
+ {
+ }
+ State operator()(const ReadyState& state)
+ {
+ return ActivatingState(state);
+ }
+
+ template <typename AnyState>
+ State operator()(const AnyState& state)
+ {
+ throw InvalidStateError("Could not mount on not empty slot");
+ }
+ };
+
+ struct UnmountEvent : public BasicEvent
+ {
+ UnmountEvent() : BasicEvent(__FUNCTION__)
+ {
+ }
+ State operator()(const ActivatingState& state)
+ {
+ return ReadyState(state);
+ }
+ State operator()(const WaitingForGadgetState& state)
+ {
+ state.machine.stopProcess(state.process);
+ return WaitingForProcessEndState(state);
+ }
+ State operator()(const ActiveState& state)
+ {
+ if (!state.machine.removeUsbGadget(state))
+ {
+ return ReadyState(state);
+ }
+ state.machine.stopProcess(state.process);
+ return WaitingForProcessEndState(state);
+ }
+ State operator()(const WaitingForProcessEndState& state)
+ {
+ throw InvalidStateError("Could not unmount on empty slot");
+ }
+ State operator()(const ReadyState& state)
+ {
+ throw InvalidStateError("Could not unmount on empty slot");
+ }
+ };
+
+ struct SubprocessStoppedEvent : public BasicEvent
+ {
+ SubprocessStoppedEvent() : BasicEvent(__FUNCTION__)
+ {
+ }
+ State operator()(const ActivatingState& state)
+ {
+ return ReadyState(state);
+ }
+ State operator()(const WaitingForGadgetState& state)
+ {
+ state.machine.stopProcess(state.process);
+ return ReadyState(state);
+ }
+ State operator()(const ActiveState& state)
+ {
+ if (!state.machine.removeUsbGadget(state))
+ {
+ return ReadyState(state);
+ }
+ return ReadyState(state);
+ }
+ State operator()(const WaitingForProcessEndState& state)
+ {
+ return ReadyState(state);
+ }
+ };
+
+ struct ActivationStartedEvent : public BasicEvent
+ {
+ ActivationStartedEvent() : BasicEvent(__FUNCTION__)
+ {
+ }
+ State operator()(const ActivatingState& state)
+ {
+ if (state.machine.config.mode == Configuration::Mode::proxy)
+ {
+ return activateProxyMode(state);
+ }
+ return activateLegacyMode(state);
+ }
+
+ State activateProxyMode(const ActivatingState& state)
+ {
+ auto process = std::make_shared<Process>(
+ state.machine.ioc.get(), state.machine.name,
+ state.machine.config.nbdDevice);
+ if (!process)
+ {
+ LogMsg(Logger::Error, state.machine.name,
+ " Failed to create Process for: ", state.machine.name);
+ return ReadyState(state);
+ }
+ if (!process->spawn(
+ Configuration::MountPoint::toArgs(state.machine.config),
+ [& machine = state.machine](int exitCode, bool isReady) {
+ LogMsg(Logger::Info, machine.name, " process ended.");
+ machine.emitSubprocessStoppedEvent();
+ }))
+ {
+ LogMsg(Logger::Error, state.machine.name,
+ " Failed to spawn Process for: ", state.machine.name);
+ return ReadyState(state);
+ }
+ auto newState = WaitingForGadgetState(state);
+ newState.process = process;
+ return newState;
+ }
+
+ State activateLegacyMode(const ActivatingState& state)
+ {
+ // Check if imgUrl is not emptry
+ if (isCifsUrl(state.machine.target->imgUrl))
+ {
+ auto newState = ActiveState(state);
+
+ return newState;
+ }
+ else
+ {
+ throw sdbusplus::exception::SdBusError(
+ EINVAL, "Not supported url's scheme.");
+ }
+ }
+
+ int prepareTempDirForLegacyMode(std::string& path)
+ {
+ int result = -1;
+ char mountPathTemplate[] = "/tmp/vm_legacy.XXXXXX";
+ const char* tmpPath = mkdtemp(mountPathTemplate);
+ if (tmpPath != nullptr)
+ {
+ path = tmpPath;
+ result = 0;
+ }
+
+ return result;
+ }
+
+ bool checkUrl(const std::string& urlScheme, const std::string& imageUrl)
+ {
+ return (urlScheme.compare(imageUrl.substr(0, urlScheme.size())) ==
+ 0);
+ }
+
+ bool getImagePathFromUrl(const std::string& urlScheme,
+ const std::string& imageUrl,
+ std::string* imagePath)
+ {
+ if (checkUrl(urlScheme, imageUrl))
+ {
+ if (imagePath != nullptr)
+ {
+ *imagePath = imageUrl.substr(urlScheme.size() - 1);
+ return true;
+ }
+ else
+ {
+ LogMsg(Logger::Error, "Invalid parameter provied");
+ return false;
+ }
+ }
+ else
+ {
+ LogMsg(Logger::Error, "Provied url does not match scheme");
+ return false;
+ }
+ }
+
+ bool isHttpsUrl(const std::string& imageUrl)
+ {
+ return checkUrl("https://", imageUrl);
+ }
+
+ bool getImagePathFromHttpsUrl(const std::string& imageUrl,
+ std::string* imagePath)
+ {
+ return getImagePathFromUrl("https://", imageUrl, imagePath);
+ }
+
+ bool isCifsUrl(const std::string& imageUrl)
+ {
+ return checkUrl("smb://", imageUrl);
+ }
+
+ bool getImagePathFromCifsUrl(const std::string& imageUrl,
+ std::string* imagePath)
+ {
+ return getImagePathFromUrl("smb://", imageUrl, imagePath);
+ }
+
+ fs::path getImagePath(const std::string& imageUrl)
+ {
+ std::string imagePath;
+
+ if (getImagePathFromHttpsUrl(imageUrl, &imagePath))
+ {
+ return fs::path(imagePath);
+ }
+ else if (getImagePathFromCifsUrl(imageUrl, &imagePath))
+ {
+ return fs::path(imagePath);
+ }
+ else
+ {
+ LogMsg(Logger::Error, "Unrecognized url's scheme encountered");
+ return fs::path("");
+ }
+ }
+ };
+
+ struct UdevStateChangeEvent : public BasicEvent
+ {
+ UdevStateChangeEvent(const StateChange& devState) :
+ BasicEvent(__FUNCTION__), devState{devState}
+ {
+ }
+ State operator()(const WaitingForGadgetState& state)
+ {
+ if (devState == StateChange::inserted)
+ {
+ int32_t ret = UsbGadget::configure(
+ state.machine.name, state.machine.config.nbdDevice,
+ devState);
+ if (ret == 0)
+ {
+ return ActiveState(state);
+ }
+ return ReadyState(state);
+ }
+ return ReadyState(state);
+ }
+
+ State operator()(const ReadyState& state)
+ {
+ if (devState == StateChange::removed)
+ {
+ LogMsg(Logger::Debug, state.machine.name,
+ " This is acceptable since udev notification is often "
+ "after process is being killed");
+ }
+ return state;
+ }
+
+ template <typename AnyState>
+ State operator()(const AnyState& state)
+ {
+ LogMsg(Logger::Info, name,
+ " Udev State: ", static_cast<int>(devState));
+ LogMsg(Logger::Critical, name,
+ " If you receiving this error, this means "
+ "your FSM is broken. Rethink!");
+ return state;
+ }
+ StateChange devState;
+ };
+
+ // Helper functions
+ bool removeUsbGadget(const BasicState& state)
+ {
+ int32_t ret = UsbGadget::configure(state.machine.name,
+ state.machine.config.nbdDevice,
+ StateChange::removed);
+ if (ret != 0)
+ {
+ // This shouldn't ever happen, perhaps best is to restart app
+ LogMsg(Logger::Critical, name, " Some serious failrue happen!");
+ return false;
+ }
+ return true;
+ }
+ void stopProcess(std::weak_ptr<Process> process)
+ {
+ if (auto ptr = process.lock())
+ {
+ ptr->stop();
+ return;
+ }
+ LogMsg(Logger::Info, name, " No process to stop");
+ }
+
+ MountPointStateMachine(boost::asio::io_context& ioc,
+ DeviceMonitor& devMonitor, const std::string& name,
+ const Configuration::MountPoint& config) :
+ ioc{ioc},
+ name{name}, config{config}, state{InitialState(*this)}
+ {
+ devMonitor.addDevice(config.nbdDevice);
+ }
+
+ MountPointStateMachine& operator=(MountPointStateMachine&& machine)
+ {
+ if (this != &machine)
+ {
+ state = std::move(machine.state);
+ name = std::move(machine.name);
+ ioc = machine.ioc;
+ config = std::move(machine.config);
+ }
+ return *this;
+ }
+
+ void emitEvent(BasicEvent&& event)
+ {
+ std::string stateName = std::visit(
+ [](const BasicState& state) { return state.stateName; }, state);
+
+ LogMsg(Logger::Debug, name, " received ", event.eventName, " while in ",
+ stateName);
+
+ state = std::visit(event, state);
+ std::visit([](BasicState& state) { state.onEnter(); }, state);
+ }
+
+ void emitRegisterDBusEvent(
+ std::shared_ptr<sdbusplus::asio::connection> bus,
+ std::shared_ptr<sdbusplus::asio::object_server> objServer)
+ {
+ emitEvent(RegisterDbusEvent(bus, objServer));
+ }
+
+ void emitMountEvent()
+ {
+ emitEvent(MountEvent());
+ }
+
+ void emitUnmountEvent()
+ {
+ emitEvent(UnmountEvent());
+ }
+
+ void emitActivationStartedEvent()
+ {
+ emitEvent(ActivationStartedEvent());
+ }
+
+ void emitSubprocessStoppedEvent()
+ {
+ emitEvent(SubprocessStoppedEvent());
+ }
+
+ void emitUdevStateChangeEvent(const NBDDevice& dev, StateChange devState)
+ {
+ if (config.nbdDevice == dev)
+ {
+ emitEvent(UdevStateChangeEvent(devState));
+ }
+ else
+ {
+ LogMsg(Logger::Debug, name, " Ignoring request.");
+ }
+ }
+
+ struct Target
+ {
+ std::string imgUrl;
+ };
+
+ std::reference_wrapper<boost::asio::io_context> ioc;
+ std::string name;
+ Configuration::MountPoint config;
+
+ std::optional<Target> target;
+ State state;
+};