aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-27 16:01:41 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-27 16:01:41 +0100
commita5f0973cba17a4addc27925c50fc3ec533221b0a (patch)
treef9df21fc3b64dfdf7a3074ac504bea10d2c707c1
parenta01c96513d28e019f495e2f24d5aac3286055782 (diff)
downloadlfvm-a5f0973cba17a4addc27925c50fc3ec533221b0a.tar.gz
lfvm-a5f0973cba17a4addc27925c50fc3ec533221b0a.tar.bz2
support for setting/changing VM options
-rw-r--r--CMakeLists.txt6
-rw-r--r--lfvm/CMakeLists.txt2
-rw-r--r--lfvm/create.cc35
-rw-r--r--lfvm/set.cc63
-rw-r--r--liblfvm/CMakeLists.txt8
-rw-r--r--liblfvm/serialize.ccm26
-rw-r--r--liblfvm/tests/CMakeLists.txt16
-rw-r--r--liblfvm/tests/vm_config.cc216
-rw-r--r--liblfvm/vm_config.cc135
-rw-r--r--liblfvm/vm_config.ccm74
m---------nihil0
11 files changed, 560 insertions, 21 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7f9ded6..3f53248 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,6 +4,8 @@ cmake_minimum_required(VERSION 3.28)
project(lfvm)
+option(LFVM_TESTS "Build lfvm's unit tests" ON)
+
set(CMAKE_CXX_STANDARD 26)
find_package(PkgConfig REQUIRED)
@@ -18,4 +20,6 @@ add_subdirectory(nihil)
add_subdirectory(liblfvm)
add_subdirectory(lfvm)
-enable_testing()
+if(LFVM_TESTS)
+ enable_testing()
+endif()
diff --git a/lfvm/CMakeLists.txt b/lfvm/CMakeLists.txt
index 780d216..6be3c4a 100644
--- a/lfvm/CMakeLists.txt
+++ b/lfvm/CMakeLists.txt
@@ -6,6 +6,8 @@ install(TARGETS lfvm DESTINATION sbin)
target_link_libraries(lfvm PRIVATE nihil nihil.config liblfvm)
target_sources(lfvm PRIVATE
lfvm.cc
+
create.cc
+ set.cc
start.cc
)
diff --git a/lfvm/create.cc b/lfvm/create.cc
index 9ce70d4..77f2d7f 100644
--- a/lfvm/create.cc
+++ b/lfvm/create.cc
@@ -6,6 +6,8 @@
#include <print>
#include <string>
+#include <unistd.h>
+
import nihil;
import liblfvm;
@@ -15,14 +17,41 @@ auto create = nihil::command("create", "<vm>",
[](int argc, char **argv) -> int
{
auto &ctx = lfvm::get_context();
+ auto options = std::vector<std::string>();
+
+ auto ch = 0;
+ while ((ch = getopt(argc, argv, "o:")) != -1) {
+ switch (ch) {
+ case 'o':
+ options.emplace_back(optarg);
+ break;
+
+ default:
+ return 1;
+ }
+ }
+ argc -= ::optind;
+ argv += ::optind;
if (argc != 2)
throw nihil::usage_error("expected VM name");
auto vmname = std::string_view(argv[1]);
- auto vm = lfvm::vm_config(vmname);
+ auto vm = lfvm::make_vm_config(vmname);
+ if (!vm) {
+ std::print(std::cerr, "{}\n", vm.error());
+ return 1;
+ }
+
+ for (auto &&option: options) {
+ if (auto r = set_vm_option(*vm, option); !r) {
+ std::print(std::cerr, "invalid option: {}: {}\n",
+ option, r.error());
+ return 1;
+ }
+ }
- auto ok = lfvm::vm_create(ctx, vm);
+ auto ok = lfvm::vm_create(ctx, *vm);
if (!ok) {
auto &err = ok.error();
@@ -32,7 +61,7 @@ auto create = nihil::command("create", "<vm>",
vmname);
else
std::print(std::cerr,
- "cannot create virtual machine '{}': {}",
+ "cannot create virtual machine '{}': {}\n",
vmname, err);
return 1;
diff --git a/lfvm/set.cc b/lfvm/set.cc
new file mode 100644
index 0000000..fad8d76
--- /dev/null
+++ b/lfvm/set.cc
@@ -0,0 +1,63 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <iostream>
+#include <print>
+#include <string>
+
+#include <unistd.h>
+
+import nihil;
+import liblfvm;
+
+namespace {
+
+auto create = nihil::command("set", "<vm> <option=value> [option=value ...]",
+[](int argc, char **argv) -> int
+{
+ auto &ctx = lfvm::get_context();
+
+ auto ch = 0;
+ while ((ch = getopt(argc, argv, "")) != -1) {
+ switch (ch) {
+ default:
+ return 1;
+ }
+ }
+ argc -= ::optind;
+ argv += ::optind;
+
+ if (argc < 2)
+ throw nihil::usage_error("expected VM name");
+
+ auto vmname = std::string_view(argv[0]);
+ auto vm = lfvm::vm_load(ctx, vmname);
+ if (!vm) {
+ std::print(std::cerr, "cannot load virtual machine {}: {}\n",
+ vmname, vm.error());
+ return 1;
+ }
+
+ --argc;
+ ++argv;
+
+ for (int i = 0; i < argc; ++i) {
+ auto option = std::string_view(argv[i]);
+
+ if (auto r = set_vm_option(*vm, option); !r) {
+ std::print(std::cerr, "{}: {}\n", vmname, r.error());
+ return 1;
+ }
+ }
+
+ auto ok = lfvm::vm_save(ctx, *vm);
+ if (!ok) {
+ std::print(std::cerr, "{}: {}\n", vmname, ok.error());
+ return 1;
+ }
+
+ return 0;
+});
+
+} // anonymous namespace
diff --git a/liblfvm/CMakeLists.txt b/liblfvm/CMakeLists.txt
index c3ca857..1496871 100644
--- a/liblfvm/CMakeLists.txt
+++ b/liblfvm/CMakeLists.txt
@@ -13,4 +13,12 @@ target_sources(liblfvm
serialize.ccm
vm.ccm
vm_config.ccm
+
+ PRIVATE
+ vm_config.cc
)
+
+if(LFVM_TESTS)
+ add_subdirectory(tests)
+ enable_testing()
+endif()
diff --git a/liblfvm/serialize.ccm b/liblfvm/serialize.ccm
index 340f565..2144225 100644
--- a/liblfvm/serialize.ccm
+++ b/liblfvm/serialize.ccm
@@ -65,7 +65,7 @@ export [[nodiscard]] auto deserialize(std::string_view text)
else
co_return std::unexpected(nihil::error("missing name"));
- auto vm = vm_config(name);
+ auto vm = co_await make_vm_config(name);
// UUID
if (auto o = uobj.find("uuid"); o) {
@@ -80,12 +80,16 @@ export [[nodiscard]] auto deserialize(std::string_view text)
// ncpus
if (auto o = uobj.find("ncpus"); o) {
auto n = co_await object_cast<integer>(*o);
- vm.ncpus(n.value());
+ if (auto ok = vm.ncpus(n.value()); !ok)
+ co_return std::unexpected(nihil::error(
+ "invalid ncpus", ok.error()));
}
if (auto o = uobj.find("memory"); o) {
auto n = co_await object_cast<integer>(*o);
- vm.memory_size(n.value());
+ if (auto ok = vm.memory_size(n.value()); !ok)
+ co_return std::unexpected(nihil::error(
+ "invalid memory size", ok.error()));
}
if (auto o = uobj.find("destroy_on_poweroff"); o) {
@@ -167,4 +171,20 @@ export [[nodiscard]] auto vm_create(context const &ctx,
co_return {};
}
+/*
+ * Save an existing VM to disk.
+ */
+export [[nodiscard]] auto vm_save(context const &ctx,
+ vm_config const &config)
+ -> std::expected<void, nihil::error>
+{
+ auto ucltext = co_await serialize(config);
+ auto filename = ctx.vm_config_file(config.name());
+
+ co_await nihil::safe_write_file(filename, ucltext);
+
+ // TODO: Perhaps sync the file?
+ co_return {};
+}
+
} // namespace lfvm
diff --git a/liblfvm/tests/CMakeLists.txt b/liblfvm/tests/CMakeLists.txt
new file mode 100644
index 0000000..e8b9c88
--- /dev/null
+++ b/liblfvm/tests/CMakeLists.txt
@@ -0,0 +1,16 @@
+# This source code is released into the public domain.
+
+add_executable(liblfvm.test
+ vm_config.cc
+)
+
+target_link_libraries(liblfvm.test PRIVATE
+ liblfvm
+ Catch2::Catch2WithMain
+)
+
+find_package(Catch2 REQUIRED)
+
+include(CTest)
+include(Catch)
+catch_discover_tests(liblfvm.test)
diff --git a/liblfvm/tests/vm_config.cc b/liblfvm/tests/vm_config.cc
new file mode 100644
index 0000000..3ba27f3
--- /dev/null
+++ b/liblfvm/tests/vm_config.cc
@@ -0,0 +1,216 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <cstdint>
+#include <limits>
+#include <system_error>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+import liblfvm;
+
+TEST_CASE("vm_config: memory_size", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ REQUIRE(vm->memory_size() > 0);
+
+ auto r = vm->memory_size(1024);
+ REQUIRE(r);
+ REQUIRE(vm->memory_size() == 1024);
+
+ r = vm->memory_size(4096);
+ REQUIRE(r);
+ REQUIRE(vm->memory_size() == 4096);
+
+ r = vm->memory_size(std::numeric_limits<std::uint64_t>::max());
+ REQUIRE(!r);
+ REQUIRE(r.error() == std::errc::value_too_large);
+ REQUIRE(vm->memory_size() == 4096);
+
+ r = vm->memory_size(0);
+ REQUIRE(!r);
+ REQUIRE(r.error() == std::errc::invalid_argument);
+ REQUIRE(vm->memory_size() == 4096);
+}
+
+TEST_CASE("set_vm_option: memory", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ auto r = set_vm_option(*vm, "memory=2g");
+ REQUIRE(r);
+ REQUIRE(vm->memory_size() ==
+ (static_cast<std::int64_t>(2) * 1024 * 1024 * 1024));
+
+ r = set_vm_option(*vm, "memory=0");
+ REQUIRE(!r);
+}
+
+TEST_CASE("vm_config: ncpus", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ REQUIRE(vm->ncpus() > 0);
+
+ auto r = vm->ncpus(1);
+ REQUIRE(r);
+ REQUIRE(vm->ncpus() == 1);
+
+ r = vm->ncpus(16);
+ REQUIRE(r);
+ REQUIRE(vm->ncpus() == 16);
+
+ r = vm->ncpus(0);
+ REQUIRE(!r);
+ REQUIRE(r.error() == std::errc::invalid_argument);
+ REQUIRE(vm->ncpus() == 16);
+}
+
+TEST_CASE("set_vm_option: ncpus", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ auto r = set_vm_option(*vm, "ncpus=2");
+ REQUIRE(r);
+ REQUIRE(vm->ncpus() == 2);
+
+ r = set_vm_option(*vm, "ncpus=0");
+ REQUIRE(!r);
+}
+
+TEST_CASE("vm_config: destroy_on_poweroff", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->destroy_on_poweroff(true);
+ REQUIRE(vm->destroy_on_poweroff() == true);
+
+ vm->destroy_on_poweroff(false);
+ REQUIRE(vm->destroy_on_poweroff() == false);
+}
+
+TEST_CASE("vm_config: wire_memory", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->wire_memory(true);
+ REQUIRE(vm->wire_memory() == true);
+
+ vm->wire_memory(false);
+ REQUIRE(vm->wire_memory() == false);
+}
+
+TEST_CASE("set_vm_option: wire_memory", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ auto r = set_vm_option(*vm, "wire_memory=yes");
+ REQUIRE(r);
+ REQUIRE(vm->wire_memory() == true);
+
+ r = set_vm_option(*vm, "wire_memory=off");
+ REQUIRE(r);
+ REQUIRE(vm->wire_memory() == false);
+
+ r = set_vm_option(*vm, "wire_memory=0");
+ REQUIRE(!r);
+}
+
+TEST_CASE("vm_config: include_memory_in_core", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->include_memory_in_core(true);
+ REQUIRE(vm->include_memory_in_core() == true);
+
+ vm->include_memory_in_core(false);
+ REQUIRE(vm->include_memory_in_core() == false);
+}
+
+TEST_CASE("set_vm_option: include_memory_in_core", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ auto r = set_vm_option(*vm, "include_memory_in_core=yes");
+ REQUIRE(r);
+ REQUIRE(vm->include_memory_in_core() == true);
+
+ r = set_vm_option(*vm, "include_memory_in_core=off");
+ REQUIRE(r);
+ REQUIRE(vm->include_memory_in_core() == false);
+
+ r = set_vm_option(*vm, "include_memory_in_core=0");
+ REQUIRE(!r);
+}
+
+TEST_CASE("vm_config: yield_on_halt", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->yield_on_halt(true);
+ REQUIRE(vm->yield_on_halt() == true);
+
+ vm->yield_on_halt(false);
+ REQUIRE(vm->yield_on_halt() == false);
+}
+
+TEST_CASE("vm_config: exit_on_pause", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->exit_on_pause(true);
+ REQUIRE(vm->exit_on_pause() == true);
+
+ vm->exit_on_pause(false);
+ REQUIRE(vm->exit_on_pause() == false);
+}
+
+TEST_CASE("vm_config: rtc_is_utc", "[lfvm]")
+{
+ using namespace lfvm;
+
+ auto vm = make_vm_config("test");
+ REQUIRE(vm);
+
+ vm->rtc_is_utc(true);
+ REQUIRE(vm->rtc_is_utc() == true);
+
+ vm->rtc_is_utc(false);
+ REQUIRE(vm->rtc_is_utc() == false);
+}
diff --git a/liblfvm/vm_config.cc b/liblfvm/vm_config.cc
new file mode 100644
index 0000000..8acb797
--- /dev/null
+++ b/liblfvm/vm_config.cc
@@ -0,0 +1,135 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <algorithm>
+#include <coroutine>
+#include <expected>
+#include <format>
+#include <functional>
+#include <ranges>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+module liblfvm;
+
+import nihil;
+
+namespace lfvm {
+
+/*
+ * Split an option into name and value.
+ */
+[[nodiscard]] auto parse_option(std::string_view str)
+ -> std::expected<std::pair<std::string_view, std::string_view>,
+ nihil::error>
+{
+ auto split = std::ranges::find(str, '=');
+ if (split == str.end())
+ return std::unexpected(nihil::error(
+ "missing '=' in option string"));
+
+ auto option = std::string_view(str.begin(), split);
+ if (option.empty())
+ return std::unexpected(nihil::error(
+ "invalid option name"));
+
+ auto value = std::string_view(std::next(split), str.end());
+ if (value.empty())
+ return std::unexpected(nihil::error(
+ "invalid option value"));
+
+ return std::make_pair(option, value);
+}
+
+/*
+ * Parse a boolean option (true/false).
+ */
+[[nodiscard]] auto parse_bool(std::string_view value)
+ -> std::expected<bool, nihil::error>
+{
+ using namespace std::literals;
+
+ static auto true_values = std::unordered_set{
+ "yes"sv, "true"sv, "enabled"sv, "on"sv
+ };
+
+ static auto false_values = std::unordered_set{
+ "no"sv, "false"sv, "disabled"sv, "off"sv
+ };
+
+ if (true_values.find(value) != true_values.end())
+ return true;
+
+ if (false_values.find(value) != false_values.end())
+ return false;
+
+ return std::unexpected(nihil::error("invalid value for boolean"));
+}
+
+auto set_vm_option(vm_config &vm, std::string_view option_string)
+ -> std::expected<void, nihil::error>
+{
+ using namespace std::literals;
+
+ // The table of options we know how to set.
+ static auto options = std::unordered_map<
+ std::string_view,
+ std::function<
+ std::expected<void, nihil::error>(
+ vm_config &, std::string_view)
+ >
+ > {
+
+std::make_pair("memory"sv,
+[](vm_config &vm, std::string_view str) -> std::expected<void, nihil::error> {
+ // Due to limitations in UCL, the memory size is limited to
+ // int64_t. In practice, this is not an issue.
+ auto new_size = co_await nihil::parse_size<std::int64_t>(str);
+ co_await vm.memory_size(new_size);
+ co_return {};
+}),
+
+std::make_pair("ncpus"sv,
+[](vm_config &vm, std::string_view str) -> std::expected<void, nihil::error> {
+ auto ncpus = co_await nihil::parse_size<unsigned>(str);
+ co_await vm.ncpus(ncpus);
+ co_return {};
+}),
+
+std::make_pair("wire_memory"sv,
+[](vm_config &vm, std::string_view str) -> std::expected<void, nihil::error> {
+ auto b = co_await parse_bool(str);
+ vm.wire_memory(b);
+ co_return {};
+}),
+
+std::make_pair("include_memory_in_core"sv,
+[](vm_config &vm, std::string_view str) -> std::expected<void, nihil::error> {
+ auto b = co_await parse_bool(str);
+ vm.include_memory_in_core(b);
+ co_return {};
+}),
+
+ };
+
+ auto [name, value] = co_await parse_option(option_string);
+
+ auto option = options.find(name);
+ if (option == options.end())
+ co_return std::unexpected(nihil::error(
+ std::format("unknown option '{}'", name)));
+
+ co_await option->second(vm, value).transform_error(
+ [&](nihil::error cause) {
+ return nihil::error(
+ std::format("failed to set {}", name),
+ std::move(cause));
+ });
+ co_return {};
+}
+
+} // namespace lfvm
diff --git a/liblfvm/vm_config.ccm b/liblfvm/vm_config.ccm
index e49a540..cd15576 100644
--- a/liblfvm/vm_config.ccm
+++ b/liblfvm/vm_config.ccm
@@ -5,7 +5,10 @@
module;
#include <cstdint>
+#include <expected>
#include <string>
+#include <system_error>
+#include <utility>
import nihil;
@@ -18,28 +21,22 @@ namespace lfvm {
*/
export struct vm_config {
- // Create a new, empty configuration.
- vm_config(std::string_view name)
- : _name(name)
- {
- }
-
// The virtual machine name.
auto name(this vm_config const &self) -> std::string_view
{
- return self._name;
+ return self.m_name;
}
// The virtual machine UUID.
// Default: a random UUID.
auto uuid(this vm_config const &self) -> nihil::uuid
{
- return self._uuid;
+ return self.m_uuid;
}
auto uuid(this vm_config &self, nihil::uuid const &new_value) -> void
{
- self._uuid = new_value;
+ self.m_uuid = new_value;
}
// How many virtual CPUs the VM will have. Default is 1.
@@ -49,9 +46,15 @@ export struct vm_config {
return self.c_flag;
}
- auto ncpus(this vm_config &self, unsigned new_value) -> void
+ [[nodiscard]] auto ncpus(this vm_config &self, unsigned new_value)
+ -> std::expected<void, nihil::error>
{
+ if (new_value == 0)
+ return std::unexpected(nihil::error(
+ std::errc::invalid_argument));
+
self.c_flag = new_value;
+ return {};
}
// How much memory to allocate to the guest.
@@ -61,9 +64,23 @@ export struct vm_config {
return self.m_flag;
}
- auto memory_size(this vm_config &self, std::uint64_t new_value) -> void
+ [[nodiscard]] auto memory_size(this vm_config &self,
+ std::uint64_t new_value)
+ -> std::expected<void, nihil::error>
{
+ // Due to limitations in UCL, memory size can't be larger
+ // than std::int64_t.
+ if (std::cmp_greater(new_value,
+ std::numeric_limits<std::int64_t>::max()))
+ return std::unexpected(nihil::error(
+ std::errc::value_too_large));
+
+ if (new_value == 0)
+ return std::unexpected(nihil::error(
+ std::errc::invalid_argument));
+
self.m_flag = new_value;
+ return {};
}
// Whether to destroy this vm_config when the guest powers off.
@@ -79,7 +96,7 @@ export struct vm_config {
self.D_flag = new_value;
}
- // Whether to wire guard memory.
+ // Whether to wire guest memory.
// Defaults to false.
auto wire_memory(this vm_config const &self) -> bool
{
@@ -140,8 +157,17 @@ export struct vm_config {
}
private:
- std::string _name;
- nihil::uuid _uuid = nihil::random_uuid();
+ friend auto make_vm_config(std::string_view name)
+ -> std::expected<vm_config, nihil::error>;
+
+ // Create a new, empty configuration.
+ vm_config(std::string_view name)
+ : m_name(name)
+ {
+ }
+
+ std::string m_name;
+ nihil::uuid m_uuid = nihil::random_uuid();
unsigned c_flag = 1; // number of CPUs
std::uint64_t m_flag = 256 * 1024 * 1024;
@@ -154,4 +180,24 @@ private:
bool u_flag = true; // RTC is UTC
};
+/*
+ * Create a new VM config for the given VM.
+ */
+export [[nodiscard]] auto make_vm_config(std::string_view name)
+ -> std::expected<vm_config, nihil::error>
+{
+ if (name.empty())
+ return std::unexpected(nihil::error(
+ "name may not be empty"));
+
+ return vm_config(name);
+}
+
+/*
+ * Set a VM option from a text string, e.g. "memory=4g".
+ */
+export [[nodiscard]] auto
+set_vm_option(vm_config &vm, std::string_view option)
+ -> std::expected<void, nihil::error>;
+
} // namespace lfvm
diff --git a/nihil b/nihil
-Subproject f7c62a0abad862149ab1a70e8610476a331f8ed
+Subproject f565a14f584f1342dd919361dc2f719c3ef4530