diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-27 16:01:41 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-27 16:01:41 +0100 |
| commit | a5f0973cba17a4addc27925c50fc3ec533221b0a (patch) | |
| tree | f9df21fc3b64dfdf7a3074ac504bea10d2c707c1 | |
| parent | a01c96513d28e019f495e2f24d5aac3286055782 (diff) | |
| download | lfvm-a5f0973cba17a4addc27925c50fc3ec533221b0a.tar.gz lfvm-a5f0973cba17a4addc27925c50fc3ec533221b0a.tar.bz2 | |
support for setting/changing VM options
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | lfvm/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | lfvm/create.cc | 35 | ||||
| -rw-r--r-- | lfvm/set.cc | 63 | ||||
| -rw-r--r-- | liblfvm/CMakeLists.txt | 8 | ||||
| -rw-r--r-- | liblfvm/serialize.ccm | 26 | ||||
| -rw-r--r-- | liblfvm/tests/CMakeLists.txt | 16 | ||||
| -rw-r--r-- | liblfvm/tests/vm_config.cc | 216 | ||||
| -rw-r--r-- | liblfvm/vm_config.cc | 135 | ||||
| -rw-r--r-- | liblfvm/vm_config.ccm | 74 | ||||
| m--------- | nihil | 0 |
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 |
