summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCzarnowski, Przemyslaw <przemyslaw.hawrylewicz.czarnowski@intel.com>2019-12-20 16:15:32 +0300
committerCzarnowski, Przemyslaw <przemyslaw.hawrylewicz.czarnowski@intel.com>2020-01-29 00:58:16 +0300
commit218c41b5f22d68b97d04fec1131208f4af93130e (patch)
treecd2fa4af79cbbb9877ac649247cce8b00b0b2334
parentfc5405d5d65dbe98b89a547304e9d66914973c46 (diff)
downloadprovingground-218c41b5f22d68b97d04fec1131208f4af93130e.tar.xz
Manage remote media state transitions
This is a first part of bigger functionality which provides host to use virtual media. First part provides skeleton and definitions of states and events defining state machine, also brings working implementation of proxy mode and some starting point to implement legacy mode. There are at least three additional patchsets implementing legacy mode with https and cifs support and secure passing of secrets. Specifically this change adds StateMachine class used to keep track state of each mount point: - StateMachine is made as std::variant of object derived from BasicState. - Each state has its own possible transitions defined (events). - Transitions defines appropriate behavior. - Specific event triggers transition from one to other specific state (1:1 relation). Tested: Manual tests on WilsonCity platform: - mounting and unmounting using redfish and webui - check state on dbus interfaces Change-Id: I4b13085e1f8884fcedd7d97e76910c21e87ab7f8 Signed-off-by: Rapkiewicz, Pawel <pawel.rapkiewicz@intel.com> Signed-off-by: Czarnowski, Przemyslaw <przemyslaw.hawrylewicz.czarnowski@intel.com>
-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;
+};