aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-27 19:18:54 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-27 19:18:54 +0100
commitf7486d8e1349cb4761a9f1a2ffddc731a79324cc (patch)
tree46f6817087f68d604bbbbb0529bd8f7a9b2f4908
parentc3384974979b3f51d1022b99ce3a56c8234e6d39 (diff)
downloadlfvm-f7486d8e1349cb4761a9f1a2ffddc731a79324cc.tar.gz
lfvm-f7486d8e1349cb4761a9f1a2ffddc731a79324cc.tar.bz2
add disk commands
-rw-r--r--lfvm/CMakeLists.txt4
-rw-r--r--lfvm/disk_create.cc76
-rw-r--r--lfvm/disk_list.cc46
-rw-r--r--lfvm/lfvm-disk.853
-rw-r--r--lfvm/lfvm.cc16
-rw-r--r--liblfvm/CMakeLists.txt2
-rw-r--r--liblfvm/context.ccm19
-rw-r--r--liblfvm/disk_config.cc170
-rw-r--r--liblfvm/disk_config.ccm96
-rw-r--r--liblfvm/liblfvm.ccm1
-rw-r--r--liblfvm/serialize.ccm8
m---------nihil0
12 files changed, 484 insertions, 7 deletions
diff --git a/lfvm/CMakeLists.txt b/lfvm/CMakeLists.txt
index 3ed9c8a..ce53735 100644
--- a/lfvm/CMakeLists.txt
+++ b/lfvm/CMakeLists.txt
@@ -11,6 +11,9 @@ target_sources(lfvm PRIVATE
set.cc
show.cc
start.cc
+
+ disk_create.cc
+ disk_list.cc
)
include(GNUInstallDirs)
@@ -18,6 +21,7 @@ include(GNUInstallDirs)
install(TARGETS lfvm DESTINATION sbin)
install(FILES
lfvm-create.8
+ lfvm-disk.8
lfvm-list.8
lfvm-set.8
lfvm-show.8
diff --git a/lfvm/disk_create.cc b/lfvm/disk_create.cc
new file mode 100644
index 0000000..e13b465
--- /dev/null
+++ b/lfvm/disk_create.cc
@@ -0,0 +1,76 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <filesystem>
+#include <iostream>
+#include <print>
+#include <string>
+
+#include <unistd.h>
+
+import nihil;
+import liblfvm;
+
+namespace {
+
+auto create = nihil::command("disk create", "<name> <file>",
+[](int argc, char **argv) -> int
+{
+ auto &ctx = lfvm::get_context();
+ auto options = std::vector<std::string>();
+
+ auto ch = 0;
+ while ((ch = getopt(argc, argv, "")) != -1) {
+ switch (ch) {
+ default:
+ return 1;
+ }
+ }
+ argc -= ::optind;
+ argv += ::optind;
+
+ if (argc != 3)
+ throw nihil::usage_error("expected name and filename");
+
+ auto diskname = std::string_view(argv[1]);
+ auto diskpath = std::string_view(argv[2]);
+
+ /*
+ * We could allow adding disks that don't exist, but this would
+ * defer failure until VM start, at which point we could only
+ * print the error to the log file. To reduce user confusion,
+ * do the check here.
+ */
+ auto ec = std::error_code();
+ if (!std::filesystem::exists(diskpath)) {
+ std::print(std::cerr, "{}: not found\n", diskpath);
+ return 1;
+ }
+
+ auto disk = lfvm::make_disk_config(diskname, diskpath);
+ if (!disk) {
+ std::print(std::cerr, "{}\n", disk.error());
+ return 1;
+ }
+
+ auto ok = lfvm::disk_create(ctx, *disk);
+ if (!ok) {
+ auto &err = ok.error();
+
+ if (err.root_cause() == std::errc::file_exists)
+ std::print(std::cerr,
+ "disk '{}' already exists\n",
+ diskname);
+ else
+ std::print(std::cerr,
+ "cannot create disk '{}': {}\n",
+ diskname, err);
+
+ return 1;
+ }
+
+ return 0;
+});
+
+} // anonymous namespace
diff --git a/lfvm/disk_list.cc b/lfvm/disk_list.cc
new file mode 100644
index 0000000..d4c2a71
--- /dev/null
+++ b/lfvm/disk_list.cc
@@ -0,0 +1,46 @@
+/*
+ * 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("disk list", "",
+[](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 > 0)
+ throw nihil::usage_error("unexpected argument");
+
+ auto disks = lfvm::disk_list(ctx);
+ if (!disks) {
+ std::print(std::cerr, "cannot load disks: {}\n", disks.error());
+ return 1;
+ }
+
+ for (auto diskname : *disks)
+ std::print("{}\n", diskname);
+
+ return 0;
+});
+
+} // anonymous namespace
diff --git a/lfvm/lfvm-disk.8 b/lfvm/lfvm-disk.8
new file mode 100644
index 0000000..7190ac0
--- /dev/null
+++ b/lfvm/lfvm-disk.8
@@ -0,0 +1,53 @@
+.\" This source code is released into the public domain.
+.Dd June 27, 2025
+.Dt LFVM-DISK 8
+.Os
+.Sh NAME
+.Nm lfvm disk
+.Nd manage virtual machine disk configurations
+.Sh SYNOPSIS
+.Nm lfvm
+.Op opts
+.Cm disk create
+.Ar disk-name
+.Ar path
+.Nm lfvm
+.Op opts
+.Cm disk list
+.Nm lfvm
+.Op opts
+.Cm disk remove
+.Ar disk-name
+.Nm lfvm
+.Op opts
+.Cm disk show
+.Ar disk-name
+.Sh DESCRIPTION
+The
+.Nm
+command is used to create, manage and destroy virtual machine disk
+configurations.
+Disk configurations are separate from any backing files (e.g., disk images);
+creating a disk configuration does not create the backing file, and
+destroying a disk does not delete the backing file.
+.Pp
+The following commands are accepted:
+.Bl -tag -width indent
+.It Cm create Ar disk-name Ar path
+Create a new disk configuration called
+.Ar disk-name
+for the backing file at
+.Ar path ,
+which must already exist.
+.It Cm list
+List existing disk configurations.
+.It Cm remove Ar disk-name
+Remove the disk configuration
+.Ar disk-name .
+The backing file is not deleted.
+.It Cm show Ar disk-name
+Display the disk configuration
+.Ar disk-name .
+.El
+.Sh SEE ALSO
+.Xr lfvm 8
diff --git a/lfvm/lfvm.cc b/lfvm/lfvm.cc
index 680a9ff..c68b87c 100644
--- a/lfvm/lfvm.cc
+++ b/lfvm/lfvm.cc
@@ -38,22 +38,30 @@ try {
argc -= ::optind;
argv += ::optind;
- // Make sure our database directory exists.
+ // Make sure our database directories exist.
+
if (auto ok = nihil::ensure_dir(ctx.dbdir()); !ok) {
- std::print(std::cerr, "cannot create {}: {}",
+ std::print(std::cerr, "cannot create {}: {}\n",
ctx.dbdir(), ok.error());
return 1;
}
if (auto ok = nihil::ensure_dir(ctx.vmconfdir()); !ok) {
- std::print(std::cerr, "cannot create {}: {}",
+ std::print(std::cerr, "cannot create {}: {}\n",
ctx.vmconfdir(), ok.error());
return 1;
}
+ if (auto ok = nihil::ensure_dir(ctx.diskconfdir()); !ok) {
+ std::print(std::cerr, "cannot create {}: {}\n",
+ ctx.diskconfdir(), ok.error());
+ return 1;
+ }
+
// Load the configuration.
if (auto ok = nihil::config::read_from(ctx.config_file()); !ok) {
- std::print(std::cerr, "cannot load configuration from {}: {}",
+ std::print(std::cerr,
+ "cannot load configuration from {}: {}\n",
ctx.config_file(), ok.error());
return 1;
}
diff --git a/liblfvm/CMakeLists.txt b/liblfvm/CMakeLists.txt
index 1d821ef..469b22b 100644
--- a/liblfvm/CMakeLists.txt
+++ b/liblfvm/CMakeLists.txt
@@ -10,11 +10,13 @@ target_sources(liblfvm
PUBLIC FILE_SET modules TYPE CXX_MODULES FILES
liblfvm.ccm
context.ccm
+ disk_config.ccm
serialize.ccm
vm.ccm
vm_config.ccm
PRIVATE
+ disk_config.cc
vm.cc
vm_config.cc
)
diff --git a/liblfvm/context.ccm b/liblfvm/context.ccm
index d6455bd..1c3057f 100644
--- a/liblfvm/context.ccm
+++ b/liblfvm/context.ccm
@@ -28,6 +28,13 @@ export struct context {
return self.dbdir() / "vm";
}
+ // The directory where disk configurations are stored (usually a
+ // subdirectory of dbdir).
+ auto diskconfdir(this context const &self) -> std::filesystem::path
+ {
+ return self.dbdir() / "disk";
+ }
+
// The path to our configuration file.
auto config_file(this context const &self) -> std::filesystem::path
{
@@ -35,13 +42,23 @@ export struct context {
}
// The configuration file for a particular VM.
- auto vm_config_file(this context const &self, std::string_view vm_name)
+ auto vm_config_file(this context const &self,
+ std::string_view vm_name)
-> std::filesystem::path
{
using namespace std::literals;
return self.vmconfdir() / (vm_name + ".ucl"s);
}
+ // The configuration file for a particular disk.
+ auto disk_config_file(this context const &self,
+ std::string_view disk_name)
+ -> std::filesystem::path
+ {
+ using namespace std::literals;
+ return self.diskconfdir() / (disk_name + ".ucl"s);
+ }
+
private:
std::filesystem::path _dbdir = LFVM_DBDIR;
};
diff --git a/liblfvm/disk_config.cc b/liblfvm/disk_config.cc
new file mode 100644
index 0000000..7c91370
--- /dev/null
+++ b/liblfvm/disk_config.cc
@@ -0,0 +1,170 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <string>
+#include <system_error>
+
+#include <fcntl.h>
+
+module liblfvm;
+
+import nihil;
+import nihil.ucl;
+
+namespace lfvm {
+
+auto make_disk_config(std::string_view name, std::string_view filename)
+ -> std::expected<disk_config, nihil::error>
+{
+ if (name.empty())
+ return std::unexpected(nihil::error("disk name missing"));
+ if (filename.empty())
+ return std::unexpected(nihil::error("disk path missing"));
+
+ return disk_config(name, filename);
+}
+
+disk_config::disk_config(std::string_view name, std::filesystem::path path)
+ : m_name(name)
+ , m_path(std::move(path))
+{
+}
+
+auto disk_config::name(this disk_config const &self) -> std::string_view
+{
+ return self.m_name;
+}
+
+auto disk_config::path(this disk_config const &self) -> std::filesystem::path const &
+{
+ return self.m_path;
+}
+
+auto disk_config::exists(this disk_config const &self) -> bool
+{
+ auto ec = std::error_code();
+ return std::filesystem::exists(self.path(), ec);
+}
+
+auto disk_config::serialize(this disk_config const &self)
+ -> std::expected<std::string, nihil::error>
+{
+ using namespace nihil::ucl;
+ using namespace std::literals;
+
+ auto uobj = map<object>();
+
+ uobj.insert({"name"sv, string(self.name())});
+ uobj.insert({"path"sv, string(self.path().string())});
+
+ return std::format("{:c}", uobj);
+}
+
+auto disk_config::deserialize(std::string_view text)
+ -> std::expected<disk_config, nihil::error>
+{
+ using namespace nihil::ucl;
+
+ auto uobj = co_await parse(text);
+
+ // Name
+ auto name = std::string();
+ if (auto o = uobj.find("name"); o)
+ name = (co_await object_cast<string>(*o)).value();
+ else
+ co_return std::unexpected(nihil::error("missing name"));
+
+ // Path
+ auto path = std::string();
+ if (auto o = uobj.find("path"); o)
+ path = (co_await object_cast<string>(*o)).value();
+ else
+ co_return std::unexpected(nihil::error("missing path"));
+
+ auto disk = co_await make_disk_config(name, path);
+ co_return disk;
+}
+
+auto disk_load(context const &ctx, std::string_view name)
+ -> std::expected<disk_config, nihil::error>
+{
+ auto filename = ctx.disk_config_file(name);
+
+ auto text = std::string();
+ co_await nihil::read_file(filename, std::back_inserter(text));
+
+ auto config = co_await disk_config::deserialize(text);
+ co_return config;
+}
+
+auto disk_create(context const &ctx, disk_config const &d)
+ -> std::expected<void, nihil::error>
+{
+ auto ucltext = co_await d.serialize();
+ auto filename = ctx.disk_config_file(d.name());
+
+ /*
+ * We can't do the usual atomic write method of writing to a temporary
+ * file and renaming because we need to open the file with O_EXCL, so
+ * instead just attempt to delete the target file if something fails.
+ */
+
+ auto file = co_await nihil::open_file(
+ filename, O_WRONLY|O_CREAT|O_EXCL, 0600);
+
+ auto file_guard = nihil::guard([&] {
+ std::filesystem::remove(filename);
+ });
+
+ co_await write(file, ucltext);
+
+ file_guard.release();
+ // TODO: Perhaps sync the file?
+ co_return {};
+}
+
+auto disk_save(context const &ctx, disk_config const &d)
+ -> std::expected<void, nihil::error>
+{
+ auto ucltext = co_await d.serialize();
+ auto filename = ctx.disk_config_file(d.name());
+
+ co_await nihil::safe_write_file(filename, ucltext);
+
+ // TODO: Perhaps sync the file?
+ co_return {};
+}
+
+auto disk_list(context const &ctx)
+ -> std::expected<std::vector<std::string>, nihil::error>
+{
+ auto ret = std::vector<std::string>();
+
+ auto files = std::filesystem::directory_iterator{ctx.diskconfdir()};
+ for (auto &&file : files) {
+ if (!file.is_regular_file())
+ continue;
+
+ auto path = file.path();
+ if (path.extension() != ".ucl")
+ continue;
+ path.replace_extension();
+
+ auto name = path.filename().string();
+ if (name[0] == '.')
+ continue;
+
+ ret.push_back(name);
+ }
+
+ return ret;
+}
+
+} // namespace lfvm
diff --git a/liblfvm/disk_config.ccm b/liblfvm/disk_config.ccm
new file mode 100644
index 0000000..975f6b9
--- /dev/null
+++ b/liblfvm/disk_config.ccm
@@ -0,0 +1,96 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+#include <string>
+
+export module liblfvm:disk_config;
+
+import nihil;
+import nihil.ucl;
+
+import :context;
+
+namespace lfvm {
+
+/*
+ * Represents a disk which can be attached to the VM.
+ */
+export struct disk_config {
+ /*
+ * The name of this disk. The name is arbitrary, it doesn't need
+ * to match the filename.
+ */
+ [[nodiscard]] auto name(this disk_config const &) -> std::string_view;
+
+ /*
+ * The on-disk filename of this disk. This is verified to exist
+ * at creation time, but it may not exist later, e.g. if the user
+ * deletes the file.
+ */
+ [[nodiscard]] auto path(this disk_config const &)
+ -> std::filesystem::path const &;
+
+ /*
+ * Check if the disk's backing file exists.
+ */
+ [[nodiscard]] auto exists(this disk_config const &) -> bool;
+
+ /*
+ * Serialize a disk to a UCL string.
+ */
+ [[nodiscard]] auto serialize(this disk_config const &)
+ -> std::expected<std::string, nihil::error>;
+
+ /*
+ * Deserialize a UCL string into a disk.
+ */
+ [[nodiscard]] static auto deserialize(std::string_view)
+ -> std::expected<disk_config, nihil::error>;
+
+private:
+ friend auto make_disk_config(std::string_view, std::string_view)
+ -> std::expected<disk_config, nihil::error>;
+
+ disk_config(std::string_view name, std::filesystem::path path);
+
+ std::string m_name;
+ std::filesystem::path m_path;
+};
+
+/*
+ * Create a new disk from a name and path.
+ */
+export [[nodiscard]] auto make_disk_config(std::string_view name,
+ std::string_view path)
+ -> std::expected<disk_config, nihil::error>;
+
+/*
+ * Load an existing disk.
+ */
+export [[nodiscard]] auto disk_load(context const &, std::string_view name)
+ -> std::expected<disk_config, nihil::error>;
+
+/*
+ * Save a new disk.
+ */
+export [[nodiscard]] auto disk_create(context const &, disk_config const &)
+ -> std::expected<void, nihil::error>;
+
+/*
+ * Save an existing disk.
+ */
+export [[nodiscard]] auto disk_save(context const &, disk_config const &)
+ -> std::expected<void, nihil::error>;
+
+/*
+ * List existing disks.
+ */
+export [[nodiscard]] auto disk_list(context const &)
+ -> std::expected<std::vector<std::string>, nihil::error>;
+
+} // namespace lfvm
diff --git a/liblfvm/liblfvm.ccm b/liblfvm/liblfvm.ccm
index 48b36a1..66d111b 100644
--- a/liblfvm/liblfvm.ccm
+++ b/liblfvm/liblfvm.ccm
@@ -7,6 +7,7 @@ module;
export module liblfvm;
export import :context;
+export import :disk_config;
export import :serialize;
export import :vm;
export import :vm_config;
diff --git a/liblfvm/serialize.ccm b/liblfvm/serialize.ccm
index b6c266f..3779704 100644
--- a/liblfvm/serialize.ccm
+++ b/liblfvm/serialize.ccm
@@ -17,11 +17,15 @@ import :vm_config;
namespace lfvm {
-// Convert a vm_config to a UCL string.
+/*
+ * Serialize a vm_config to a UCL string.
+ */
export [[nodiscard]] auto serialize(vm_config const &)
-> std::expected<std::string, nihil::error>;
-// Convert a UCL string to a vm_config.
+/*
+ * Deserialize a UCL string into a vm.
+ */
export [[nodiscard]] auto deserialize(std::string_view)
-> std::expected<vm_config, nihil::error>;
diff --git a/nihil b/nihil
-Subproject f565a14f584f1342dd919361dc2f719c3ef4530
+Subproject 892c552fb17988f53266058be051d47f9d9660e