aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-23 22:33:05 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-23 22:33:05 +0100
commit1c57eacab858c41cd5565d468982094e46312b86 (patch)
tree6fd1b0f1d035663e12863124a0b102bfb98fe10e
downloadlfvm-1c57eacab858c41cd5565d468982094e46312b86.tar.gz
lfvm-1c57eacab858c41cd5565d468982094e46312b86.tar.bz2
initial commit
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules4
-rw-r--r--CMakeLists.txt19
-rw-r--r--lfvm/CMakeLists.txt9
-rw-r--r--lfvm/lfvm.cc58
-rw-r--r--liblfvm/CMakeLists.txt16
-rw-r--r--liblfvm/context.ccm41
-rw-r--r--liblfvm/liblfvm.ccm12
-rw-r--r--liblfvm/serialize.ccm141
-rw-r--r--liblfvm/vm.ccm99
-rw-r--r--liblfvm/vm_config.ccm157
m---------nihil0
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