diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-23 22:33:05 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-23 22:33:05 +0100 |
| commit | 1c57eacab858c41cd5565d468982094e46312b86 (patch) | |
| tree | 6fd1b0f1d035663e12863124a0b102bfb98fe10e | |
| download | lfvm-1c57eacab858c41cd5565d468982094e46312b86.tar.gz lfvm-1c57eacab858c41cd5565d468982094e46312b86.tar.bz2 | |
initial commit
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | .gitmodules | 4 | ||||
| -rw-r--r-- | CMakeLists.txt | 19 | ||||
| -rw-r--r-- | lfvm/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | lfvm/lfvm.cc | 58 | ||||
| -rw-r--r-- | liblfvm/CMakeLists.txt | 16 | ||||
| -rw-r--r-- | liblfvm/context.ccm | 41 | ||||
| -rw-r--r-- | liblfvm/liblfvm.ccm | 12 | ||||
| -rw-r--r-- | liblfvm/serialize.ccm | 141 | ||||
| -rw-r--r-- | liblfvm/vm.ccm | 99 | ||||
| -rw-r--r-- | liblfvm/vm_config.ccm | 157 | ||||
| m--------- | nihil | 0 |
12 files changed, 558 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3207fe1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +*.sw? diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d59329e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "nihil"] + path = nihil + url = https://git.le-fay.org/lf/nihil + branch = main diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e03f479 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +# This source code is released into the public domain. + +cmake_minimum_required(VERSION 3.28) + +project(lfvm) + +set(CMAKE_CXX_STANDARD 26) + +find_package(PkgConfig REQUIRED) + +add_compile_options(-W) +add_compile_options(-Wall) +add_compile_options(-Wextra) +add_compile_options(-Werror) +add_compile_options(-Wpedantic) + +add_subdirectory(nihil) +add_subdirectory(liblfvm) +add_subdirectory(lfvm) diff --git a/lfvm/CMakeLists.txt b/lfvm/CMakeLists.txt new file mode 100644 index 0000000..aec9d3e --- /dev/null +++ b/lfvm/CMakeLists.txt @@ -0,0 +1,9 @@ +# This source code is released into the public domain. + +add_executable(lfvm) +install(TARGETS lfvm DESTINATION sbin) + +target_link_libraries(lfvm PRIVATE nihil nihil.config liblfvm) +target_sources(lfvm PRIVATE + lfvm.cc +) diff --git a/lfvm/lfvm.cc b/lfvm/lfvm.cc new file mode 100644 index 0000000..f610087 --- /dev/null +++ b/lfvm/lfvm.cc @@ -0,0 +1,58 @@ +/* + * This source code is released into the public domain. + */ + +#include <iostream> +#include <print> +#include <stdexcept> + +#include <sys/stat.h> +#include <stdlib.h> // setprogname() +#include <unistd.h> + +import nihil; +import nihil.config; +import liblfvm; + +using namespace std::literals; + +auto main(int argc, char **argv) -> int +try { + auto ctx = lfvm::context{}; + + // Make sure argv[0] is set so we can call setprogname(); + if (argc < 1) + throw nihil::usage_error("not enough arguments"); + + ::setprogname(argv[0]); + + // Parse command-line arguments. + auto ch = int{}; + while ((ch = ::getopt(argc, argv, "")) != -1) { + switch (ch) { + case '?': + default: + throw nihil::usage_error("unknown argument"); + } + } + argc -= ::optind; + argv += ::optind; + + // Make sure our database directory exists. + nihil::ensure_dir(ctx.dbdir()); + nihil::ensure_dir(ctx.vmconfdir()); + + // Load the configuration. + nihil::config::read_from(ctx.config_file()); + + // Dispatch the top-level command. + return nihil::dispatch_command(ctx, argc, argv); + +} catch (nihil::usage_error const &) { + nihil::print_usage<lfvm::context>("usage: lfvm"sv); + return 1; + +} catch (std::runtime_error const &exc) { + std::print(std::cerr, "{0}\n", exc.what()); + return 1; +} diff --git a/liblfvm/CMakeLists.txt b/liblfvm/CMakeLists.txt new file mode 100644 index 0000000..c3ca857 --- /dev/null +++ b/liblfvm/CMakeLists.txt @@ -0,0 +1,16 @@ +# This source code is released into the public domain. + +add_library(liblfvm STATIC) + +# TODO: This should be configurable. +target_compile_definitions(liblfvm PRIVATE LFVM_DBDIR="/var/db/lfvm") + +target_link_libraries(liblfvm PRIVATE nihil nihil.ucl nihil.config) +target_sources(liblfvm + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + liblfvm.ccm + context.ccm + serialize.ccm + vm.ccm + vm_config.ccm +) diff --git a/liblfvm/context.ccm b/liblfvm/context.ccm new file mode 100644 index 0000000..0dcc51d --- /dev/null +++ b/liblfvm/context.ccm @@ -0,0 +1,41 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <filesystem> + +export module liblfvm:context; + +namespace lfvm { + +/* + * Our global context. + */ + +export struct context { + // The database directory where our configuration is stored. + auto dbdir(this context const &self) -> std::filesystem::path + { + return self._dbdir; + } + + // The directory where VM configurations are stored (usually a + // subdirectory of dbdir). + auto vmconfdir(this context const &self) -> std::filesystem::path + { + return self.dbdir() / "vm"; + } + + // The path to our configuration file. + auto config_file(this context const &self) -> std::filesystem::path + { + return self.dbdir() / "config.ucl"; + } + +private: + std::filesystem::path _dbdir = LFVM_DBDIR; +}; + +} // namespace lfvm diff --git a/liblfvm/liblfvm.ccm b/liblfvm/liblfvm.ccm new file mode 100644 index 0000000..48b36a1 --- /dev/null +++ b/liblfvm/liblfvm.ccm @@ -0,0 +1,12 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module liblfvm; + +export import :context; +export import :serialize; +export import :vm; +export import :vm_config; diff --git a/liblfvm/serialize.ccm b/liblfvm/serialize.ccm new file mode 100644 index 0000000..4b7ed51 --- /dev/null +++ b/liblfvm/serialize.ccm @@ -0,0 +1,141 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <format> +#include <stdexcept> +#include <string> + +import nihil; +import nihil.ucl; + +export module liblfvm:serialize; + +import :vm_config; + +namespace lfvm { + +/* + * Exception thrown when (de)serialization fails. + */ +export struct serialization_error final : std::runtime_error +{ + serialization_error(std::string what) + : std::runtime_error(std::move(what)) + { + } +}; + +// Convert a vm_config to a UCL string. +export auto serialize(vm_config const &config) -> std::string +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto uobj = map<object>(); + + uobj.insert({"name"sv, string(config.name())}); + uobj.insert({"uuid"sv, string(to_string(config.uuid()))}); + uobj.insert({"ncpus"sv, integer(config.ncpus())}); + uobj.insert({"memory_size"sv, integer(config.memory_size())}); + uobj.insert({"destroy_on_poweroff"sv, + integer(config.destroy_on_poweroff())}); + uobj.insert({"wire_memory"sv, + integer(config.wire_memory())}); + uobj.insert({"include_memory_in_core"sv, + integer(config.include_memory_in_core())}); + uobj.insert({"yield_on_halt"sv, integer(config.yield_on_halt())}); + uobj.insert({"exit_on_pause"sv, integer(config.yield_on_halt())}); + uobj.insert({"rtc_is_utc"sv, integer(config.rtc_is_utc())}); + + return std::format("{:c}", uobj); +} + +// Convert a UCL string to a vm_config. +export auto deserialize(std::string_view text) -> vm_config +{ + using namespace nihil::ucl; + + auto uobj = parse(text); + + // Name + auto name_obj = uobj.find("name"); + if (!name_obj) + throw serialization_error("missing name"); + auto name = std::string(); + try { + name = object_cast<string>(*name_obj).value(); + } catch (type_mismatch const &) { + throw serialization_error("invalid name"); + } + + auto vm = vm_config(name); + + // UUID + auto uuid_obj = uobj.find("uuid"); + if (!uuid_obj) + throw serialization_error("missing uuid"); + try { + auto uuid_str = object_cast<string>(*uuid_obj); + auto uuid = nihil::uuid::from_string(uuid_str.value()); + if (!uuid) + throw serialization_error("invalid uuid"); + vm.uuid(*uuid); + } catch (type_mismatch const &) { + throw serialization_error("invalid uuid"); + } + + if (auto obj = uobj.find("ncpus"); obj) try { + vm.ncpus(object_cast<integer>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid ncpus"); + } + + if (auto obj = uobj.find("memory"); obj) try { + vm.memory_size(object_cast<integer>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid memory"); + } + + if (auto obj = uobj.find("destroy_on_poweroff"); obj) try { + vm.destroy_on_poweroff(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid destroy_on_poweroff"); + } + + if (auto obj = uobj.find("wire_memory"); obj) try { + vm.wire_memory(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid wire_memory"); + } + + if (auto obj = uobj.find("include_memory_in_core"); obj) try { + vm.include_memory_in_core(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid include_memory_in_core"); + } + + if (auto obj = uobj.find("yield_on_halt"); obj) try { + vm.yield_on_halt(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid yield_on_halt"); + } + + if (auto obj = uobj.find("exit_on_pause"); obj) try { + vm.exit_on_pause(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid exit_on_pause"); + } + + if (auto obj = uobj.find("rtc_is_utc"); obj) try { + vm.rtc_is_utc(object_cast<boolean>(*obj).value()); + } catch (type_mismatch const &) { + throw serialization_error("invalid rtc_is_utc"); + } + + return vm; +} + +} // namespace lfvm diff --git a/liblfvm/vm.ccm b/liblfvm/vm.ccm new file mode 100644 index 0000000..dd3482c --- /dev/null +++ b/liblfvm/vm.ccm @@ -0,0 +1,99 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <iostream> +#include <ranges> +#include <string> +#include <vector> + +import nihil; + +export module liblfvm:vm; + +import :vm_config; + +namespace lfvm { + +/* + * Create the bhyve arguments list for a VM configuration. + */ +auto get_bhyve_args(vm_config const &config) -> nihil::argv +{ + using namespace std::literals; + + auto args = std::vector<std::string>(); + + // UUID + args.emplace_back(std::format("-U {}", to_string(config.uuid()))); + + // CPUs + args.emplace_back(std::format("-c {}", config.ncpus())); + + // Memory + args.emplace_back(std::format("-m {}", config.memory_size())); + + if (config.destroy_on_poweroff()) + args.emplace_back("-D"sv); + + if (config.wire_memory()) + args.emplace_back("-S"sv); + + if (config.include_memory_in_core()) + args.emplace_back("-C"sv); + + if (config.yield_on_halt()) + args.emplace_back("-H"sv); + + if (config.exit_on_pause()) + args.emplace_back("-P"sv); + + if (config.rtc_is_utc()) + args.emplace_back("-u"sv); + + // VM name + args.emplace_back(config.name()); + + std::cerr << "args: ["; + for (auto &&arg : args) + std::cerr << arg << ' '; + std::cerr << "]\n"; + + return nihil::argv::from_range(std::move(args)); +} + +/* + * Represents a VM which can be started and stopped. + * Destroying the VM object will also destroy the VM. + */ + +export struct vm final { + // Create a new VM object. This doesn't create the actual VM. + vm(vm_config config) + : _config(std::move(config)) + { + } + + ~vm() { + // TODO: Destroy the VM. + } + + // Get the configuration for this VM. + auto config(this vm const &self) -> vm_config const & + { + return self._config; + } + + // Start the VM and wait until bhyve exits. + auto run(this vm &self) -> void + { + auto argv = get_bhyve_args(self.config()); + } + +private: + vm_config _config; +}; + +} // namespace lfvm diff --git a/liblfvm/vm_config.ccm b/liblfvm/vm_config.ccm new file mode 100644 index 0000000..e49a540 --- /dev/null +++ b/liblfvm/vm_config.ccm @@ -0,0 +1,157 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdint> +#include <string> + +import nihil; + +export module liblfvm:vm_config; + +namespace lfvm { + +/* + * Represents the configuration for a virtual machine. + */ + +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; + } + + // The virtual machine UUID. + // Default: a random UUID. + auto uuid(this vm_config const &self) -> nihil::uuid + { + return self._uuid; + } + + auto uuid(this vm_config &self, nihil::uuid const &new_value) -> void + { + self._uuid = new_value; + } + + // How many virtual CPUs the VM will have. Default is 1. + // TODO: support the full bhyve CPU topology here. + auto ncpus(this vm_config const &self) -> unsigned + { + return self.c_flag; + } + + auto ncpus(this vm_config &self, unsigned new_value) -> void + { + self.c_flag = new_value; + } + + // How much memory to allocate to the guest. + // Default is 256MB. + auto memory_size(this vm_config const &self) -> std::uint64_t + { + return self.m_flag; + } + + auto memory_size(this vm_config &self, std::uint64_t new_value) -> void + { + self.m_flag = new_value; + } + + // Whether to destroy this vm_config when the guest powers off. + // Defaults to true. + auto destroy_on_poweroff(this vm_config const &self) -> bool + { + return self.D_flag; + } + + auto destroy_on_poweroff(this vm_config &self, bool new_value) + -> void + { + self.D_flag = new_value; + } + + // Whether to wire guard memory. + // Defaults to false. + auto wire_memory(this vm_config const &self) -> bool + { + return self.S_flag; + } + + auto wire_memory(this vm_config &self, bool new_value) -> void + { + self.S_flag = new_value; + } + + // Whether to include memory in core files. + // Defaults to false. + auto include_memory_in_core(this vm_config const &self) -> bool + { + return self.C_flag; + } + + auto include_memory_in_core(this vm_config &self, bool new_value) -> void + { + self.C_flag = new_value; + } + + // Whether to yield when the guest issues a HLT instruction. + // Defaults to true. + auto yield_on_halt(this vm_config const &self) -> bool + { + return self.H_flag; + } + + auto yield_on_halt(this vm_config &self, bool new_value) -> void + { + self.H_flag = new_value; + } + + // Whether to exit when the guest issues a PAUSE instruction. + // Defaults to true. + auto exit_on_pause(this vm_config const &self) -> bool + { + return self.P_flag; + } + + auto exit_on_pause(this vm_config &self, bool new_value) -> void + { + self.P_flag = new_value; + } + + // Whether the RTC keeps UTC time. + // Defaults to true. + auto rtc_is_utc(this vm_config const &self) -> bool + { + return self.u_flag; + } + + auto rtc_is_utc(this vm_config &self, bool new_value) -> void + { + self.u_flag = new_value; + } + +private: + std::string _name; + nihil::uuid _uuid = nihil::random_uuid(); + + unsigned c_flag = 1; // number of CPUs + std::uint64_t m_flag = 256 * 1024 * 1024; + // guest memory size + bool C_flag = false; // include memory in core dumps + bool D_flag = true; // destroy VM on poweroff + bool H_flag = true; // yield on HLT + bool P_flag = true; // exit on PAUSE + bool S_flag = false; // wire guest memory + bool u_flag = true; // RTC is UTC +}; + +} // namespace lfvm diff --git a/nihil b/nihil new file mode 160000 +Subproject c2f52d8d9e76ea0df0246b0efabc96a68ceefea |
