diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-27 19:18:54 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-27 19:18:54 +0100 |
| commit | f7486d8e1349cb4761a9f1a2ffddc731a79324cc (patch) | |
| tree | 46f6817087f68d604bbbbb0529bd8f7a9b2f4908 | |
| parent | c3384974979b3f51d1022b99ce3a56c8234e6d39 (diff) | |
| download | lfvm-f7486d8e1349cb4761a9f1a2ffddc731a79324cc.tar.gz lfvm-f7486d8e1349cb4761a9f1a2ffddc731a79324cc.tar.bz2 | |
add disk commands
| -rw-r--r-- | lfvm/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | lfvm/disk_create.cc | 76 | ||||
| -rw-r--r-- | lfvm/disk_list.cc | 46 | ||||
| -rw-r--r-- | lfvm/lfvm-disk.8 | 53 | ||||
| -rw-r--r-- | lfvm/lfvm.cc | 16 | ||||
| -rw-r--r-- | liblfvm/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | liblfvm/context.ccm | 19 | ||||
| -rw-r--r-- | liblfvm/disk_config.cc | 170 | ||||
| -rw-r--r-- | liblfvm/disk_config.ccm | 96 | ||||
| -rw-r--r-- | liblfvm/liblfvm.ccm | 1 | ||||
| -rw-r--r-- | liblfvm/serialize.ccm | 8 | ||||
| m--------- | nihil | 0 |
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 |
