diff --git a/3rd-party/CMakeLists.txt b/3rd-party/CMakeLists.txt index 6bca077ef8b..d122905d78b 100644 --- a/3rd-party/CMakeLists.txt +++ b/3rd-party/CMakeLists.txt @@ -54,7 +54,7 @@ function(generate_grpc_cpp SRCS DEST) "${DEST}/${FIL_WE}.pb.cc" "${DEST}/${FIL_WE}.pb.h" COMMAND $ - ARGS --grpc_out=${DEST} --cpp_out=${DEST} --proto_path=${FIL_DIR} --plugin=protoc-gen-grpc=$ ${ABS_FIL} + ARGS --grpc_out=${DEST} --cpp_out=${DEST} --proto_path=${FIL_DIR} --proto_path=${grpc_SOURCE_DIR}/third_party/protobuf/src --plugin=protoc-gen-grpc=$ ${ABS_FIL} DEPENDS ${ABS_FIL} protoc grpc_cpp_plugin COMMENT "Running gRPC C++ protocol buffer compiler on ${FIL}" VERBATIM) diff --git a/completions/bash/multipass b/completions/bash/multipass index 6e9aa70e08a..3b78fc700ee 100644 --- a/completions/bash/multipass +++ b/completions/bash/multipass @@ -18,14 +18,43 @@ _multipass_complete() { local state=$1 - local cmd="multipass list --format=csv --no-ipv4" - [ -n "$state" ] && cmd="$cmd | \tail -n +2 | \awk -F',' '\$2 == \"$state\"'" + local cmd="multipass list --format=csv --no-ipv4 | \tail -n +2" + [ -n "$state" ] && cmd="$cmd | \awk -F',' '\$2 == \"$state\"'" local instances=$( \eval $cmd | \cut -d',' -f 1 | \tr '\r\n' ' ') _add_nonrepeating_args "$instances" } + _multipass_snapshots() + { + local instance=$1 + local cmd="multipass list --snapshots --format=csv 2>/dev/null | \tail -n +2" + [ -n "$instance" ] && cmd="$cmd | \awk -F',' '\$1 == \"$instance\"'" + local snapshots=$( \eval $cmd | \cut -d',' -f 1,2 | \tr ',' '.' | \tr '\r\n' ' ' ) + + _add_nonrepeating_args "$snapshots" + } + + _multipass_instances_and_snapshots() + { + _multipass_snapshots + _multipass_instances + } + + _multipass_restorable_snapshots() + { + local instances=$( multipass info --no-runtime-information --format=csv \ + | \tail -n +2 \ + | \awk -F',|\r?\n' '$2 == "Stopped" && $16 > 0' \ + | \cut -d',' -f 1 \ + | \tr '\r\n' ' ') + + for instance in ${instances}; do + _multipass_snapshots "${instance}" + done + } + # Set $opts to the list of available networks. _multipass_networks() { @@ -186,7 +215,7 @@ _multipass_complete() cmd="${COMP_WORDS[1]}" prev_opts=false multipass_cmds="authenticate transfer delete exec find help info launch list mount networks \ - purge recover shell start stop suspend restart umount version get set \ + purge recover restart restore shell snapshot start stop suspend umount version get set \ alias aliases unalias" if [[ "${multipass_cmds}" =~ " ${cmd} " || "${multipass_cmds}" =~ ^${cmd} || "${multipass_cmds}" =~ \ ${cmd}$ ]]; @@ -200,9 +229,12 @@ _multipass_complete() opts="${opts} --working-directory --no-map-working-directory" ;; "info") - _add_nonrepeating_args "--all --format" + _add_nonrepeating_args "--format --snapshots" ;; - "list"|"ls"|"networks"|"aliases") + "list"|"ls") + _add_nonrepeating_args "--format --snapshots" + ;; + "networks"|"aliases") _add_nonrepeating_args "--format" ;; "delete") @@ -231,6 +263,12 @@ _multipass_complete() "transfer"|"copy-files") _add_nonrepeating_args "--parents --recursive" ;; + "snapshot") + _add_nonrepeating_args "--name --comment" + ;; + "restore") + _add_nonrepeating_args "--destructive" + ;; esac if [[ ${prev} == -* ]]; then @@ -302,12 +340,21 @@ _multipass_complete() _multipass_instances "Stopped" _multipass_instances "Suspended" ;; - "delete"|"info"|"umount"|"unmount") + "umount"|"unmount") _multipass_instances ;; + "delete"|"info") + _multipass_instances_and_snapshots + ;; "recover") _multipass_instances "Deleted" ;; + "snapshot") + _multipass_instances "Stopped" + ;; + "restore") + _multipass_restorable_snapshots + ;; "mount") local source_set=0 local prev diff --git a/include/multipass/cli/format_utils.h b/include/multipass/cli/format_utils.h index 38f4877f4ce..d0367276d99 100644 --- a/include/multipass/cli/format_utils.h +++ b/include/multipass/cli/format_utils.h @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -33,44 +34,77 @@ class Formatter; namespace format { +static constexpr int col_buffer = 3; + std::string status_string_for(const InstanceStatus& status); std::string image_string_for(const multipass::FindReply_AliasInfo& alias); Formatter* formatter_for(const std::string& format); -template -Instances sorted(const Instances& instances); +template +Container sorted(const Container& items); void filter_aliases(google::protobuf::RepeatedPtrField& aliases); // Computes the column width needed to display all the elements of a range [begin, end). get_width is a function // which takes as input the element in the range and returns its width in columns. -static constexpr auto column_width = [](const auto begin, const auto end, const auto get_width, int minimum_width, - int space = 2) { - if (0 == std::distance(begin, end)) - return minimum_width; - - auto max_width = - std::max_element(begin, end, [&get_width](auto& lhs, auto& rhs) { return get_width(lhs) < get_width(rhs); }); - return std::max(get_width(*max_width) + space, minimum_width); -}; +static constexpr auto column_width = + [](const auto begin, const auto end, const auto get_width, int header_width, int minimum_width = 0) { + if (0 == std::distance(begin, end)) + return std::max({header_width + col_buffer, minimum_width}); + + auto max_width = std::max_element(begin, end, [&get_width](auto& lhs, auto& rhs) { + return get_width(lhs) < get_width(rhs); + }); + return std::max({get_width(*max_width) + col_buffer, header_width + col_buffer, minimum_width}); + }; } // namespace format } // namespace multipass -template -Instances multipass::format::sorted(const Instances& instances) +template +Container multipass::format::sorted(const Container& items) { - if (instances.empty()) - return instances; + if (items.empty()) + return items; - auto ret = instances; + auto ret = items; const auto petenv_name = MP_SETTINGS.get(petenv_key).toStdString(); std::sort(std::begin(ret), std::end(ret), [&petenv_name](const auto& a, const auto& b) { - if (a.name() == petenv_name) + using T = std::decay_t; + using google::protobuf::util::TimeUtil; + + // Put instances first when sorting info reply + if constexpr (std::is_same_v) + { + if (a.has_instance_info() && b.has_snapshot_info()) + return true; + else if (a.has_snapshot_info() && b.has_instance_info()) + return false; + } + + // Put petenv related entries first + if (a.name() == petenv_name && b.name() != petenv_name) return true; - else if (b.name() == petenv_name) + else if (b.name() == petenv_name && a.name() != petenv_name) return false; else + { + // Sort by timestamp when names are the same for snapshots + if constexpr (std::is_same_v) + { + if (a.has_snapshot_info() && a.name() == b.name()) + return TimeUtil::TimestampToNanoseconds(a.snapshot_info().fundamentals().creation_timestamp()) < + TimeUtil::TimestampToNanoseconds(b.snapshot_info().fundamentals().creation_timestamp()); + } + else if constexpr (std::is_same_v) + { + if (a.name() == b.name()) + return TimeUtil::TimestampToNanoseconds(a.fundamentals().creation_timestamp()) < + TimeUtil::TimestampToNanoseconds(b.fundamentals().creation_timestamp()); + } + + // Lastly, sort by name return a.name() < b.name(); + } }); return ret; diff --git a/include/multipass/exceptions/file_open_failed_exception.h b/include/multipass/exceptions/file_open_failed_exception.h new file mode 100644 index 00000000000..b1416e9b3c4 --- /dev/null +++ b/include/multipass/exceptions/file_open_failed_exception.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_FILE_OPEN_FAILED_EXCEPTION_H +#define MULTIPASS_FILE_OPEN_FAILED_EXCEPTION_H + +#include +#include +#include + +namespace multipass +{ +class FileOpenFailedException : public std::runtime_error +{ +public: + explicit FileOpenFailedException(const std::string& name) + : std::runtime_error(fmt::format("failed to open file '{}': {}({})", name, strerror(errno), errno)) + { + } +}; +} // namespace multipass + +#endif // MULTIPASS_FILE_OPEN_FAILED_EXCEPTION_H diff --git a/include/multipass/exceptions/snapshot_exceptions.h b/include/multipass/exceptions/snapshot_exceptions.h new file mode 100644 index 00000000000..b1974b41411 --- /dev/null +++ b/include/multipass/exceptions/snapshot_exceptions.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_SNAPSHOT_EXCEPTIONS_H +#define MULTIPASS_SNAPSHOT_EXCEPTIONS_H + +#include +#include + +#include + +namespace multipass +{ +class SnapshotNameTakenException : public std::runtime_error +{ +public: + SnapshotNameTakenException(const std::string& vm_name, const std::string& snapshot_name) + : std::runtime_error{fmt::format("Snapshot already exists: {}.{}", vm_name, snapshot_name)} + { + } +}; + +class NoSuchSnapshotException : public std::runtime_error +{ +public: + NoSuchSnapshotException(const std::string& vm_name, const std::string& snapshot_name) + : std::runtime_error{fmt::format("No such snapshot: {}.{}", vm_name, snapshot_name)} + { + } +}; +} // namespace multipass + +#endif // MULTIPASS_SNAPSHOT_EXCEPTIONS_H diff --git a/include/multipass/file_ops.h b/include/multipass/file_ops.h index d62af532c75..ced9e1f5f1e 100644 --- a/include/multipass/file_ops.h +++ b/include/multipass/file_ops.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,10 @@ class FileOps : public Singleton // QDir operations virtual bool exists(const QDir& dir) const; virtual bool isReadable(const QDir& dir) const; + virtual QFileInfoList entryInfoList(const QDir& dir, + const QStringList& nameFilters, + QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; virtual bool mkpath(const QDir& dir, const QString& dirName) const; virtual bool rmdir(QDir& dir, const QString& dirName) const; diff --git a/include/multipass/json_utils.h b/include/multipass/json_utils.h index 2b08d86b587..daa6875d6d1 100644 --- a/include/multipass/json_utils.h +++ b/include/multipass/json_utils.h @@ -13,21 +13,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * Authored by: Alberto Aguirre - * */ #ifndef MULTIPASS_JSON_UTILS_H #define MULTIPASS_JSON_UTILS_H -#include +#include "singleton.h" #include #include +#include + +#define MP_JSONUTILS multipass::JsonUtils::instance() + namespace multipass { -void write_json(const QJsonObject& root, QString file_name); -std::string json_to_string(const QJsonObject& root); -} +class JsonUtils : public Singleton +{ +public: + explicit JsonUtils(const Singleton::PrivatePass&) noexcept; + + virtual void write_json(const QJsonObject& root, QString file_name) const; // transactional; creates parent dirs + virtual std::string json_to_string(const QJsonObject& root) const; +}; +} // namespace multipass #endif // MULTIPASS_JSON_UTILS_H diff --git a/include/multipass/memory_size.h b/include/multipass/memory_size.h index 1585f25c4c7..0e7ae0e4247 100644 --- a/include/multipass/memory_size.h +++ b/include/multipass/memory_size.h @@ -32,7 +32,7 @@ class MemorySize friend bool operator<=(const MemorySize& a, const MemorySize& b); friend bool operator>=(const MemorySize& a, const MemorySize& b); - MemorySize(); + MemorySize() noexcept; explicit MemorySize(const std::string& val); long long in_bytes() const noexcept; long long in_kilobytes() const noexcept; diff --git a/include/multipass/mount_handler.h b/include/multipass/mount_handler.h index 83f18aa74fd..11e387dbb8e 100644 --- a/include/multipass/mount_handler.h +++ b/include/multipass/mount_handler.h @@ -67,6 +67,11 @@ class MountHandler : private DisabledCopyMove active = false; } + const VMMount& get_mount_spec() const noexcept + { + return mount_spec; + } + virtual bool is_active() { return active; @@ -79,9 +84,11 @@ class MountHandler : private DisabledCopyMove protected: MountHandler() = default; - MountHandler(VirtualMachine* vm, const SSHKeyProvider* ssh_key_provider, const std::string& target, - const std::string& source) - : vm{vm}, ssh_key_provider{ssh_key_provider}, target{target}, source{source}, active{false} + MountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + VMMount mount_spec, + const std::string& target) + : vm{vm}, ssh_key_provider{ssh_key_provider}, mount_spec{std::move(mount_spec)}, target{target}, active{false} { std::error_code err; auto source_status = MP_FILEOPS.status(source, err); @@ -114,9 +121,9 @@ class MountHandler : private DisabledCopyMove VirtualMachine* vm; const SSHKeyProvider* ssh_key_provider; + const VMMount mount_spec = {}; const std::string target; - const std::string source; - + const std::string& source = mount_spec.source_path; bool active; std::mutex active_mutex; }; diff --git a/include/multipass/snapshot.h b/include/multipass/snapshot.h new file mode 100644 index 00000000000..dc0f10eee0d --- /dev/null +++ b/include/multipass/snapshot.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_SNAPSHOT_H +#define MULTIPASS_SNAPSHOT_H + +#include "disabled_copy_move.h" +#include "virtual_machine.h" + +#include +#include +#include + +class QJsonObject; +class QDateTime; + +namespace multipass +{ +class MemorySize; +struct VMMount; + +class Snapshot : private DisabledCopyMove +{ +public: + virtual ~Snapshot() = default; + + virtual int get_index() const noexcept = 0; + virtual std::string get_name() const = 0; + virtual std::string get_comment() const = 0; + virtual QDateTime get_creation_timestamp() const noexcept = 0; + virtual int get_num_cores() const noexcept = 0; + virtual MemorySize get_mem_size() const noexcept = 0; + virtual MemorySize get_disk_space() const noexcept = 0; + virtual VirtualMachine::State get_state() const noexcept = 0; + + // Note that these return references - careful not to delete the snapshot while they are in use + virtual const std::unordered_map& get_mounts() const noexcept = 0; + virtual const QJsonObject& get_metadata() const noexcept = 0; + + virtual std::shared_ptr get_parent() const = 0; + virtual std::shared_ptr get_parent() = 0; + virtual std::string get_parents_name() const = 0; + virtual int get_parents_index() const = 0; + + // precondition for setters: call only on captured snapshots + virtual void set_name(const std::string&) = 0; + virtual void set_comment(const std::string&) = 0; + virtual void set_parent(std::shared_ptr) = 0; + + // precondition: capture only once + virtual void capture() = 0; // not using the constructor, we need snapshot objects for existing snapshots too + // precondition: call only on captured snapshots + virtual void erase() = 0; // not using the destructor, we want snapshots to stick around when daemon quits + // precondition: call only on captured snapshots + virtual void apply() = 0; +}; +} // namespace multipass + +#endif // MULTIPASS_SNAPSHOT_H diff --git a/include/multipass/sshfs_mount/sshfs_mount_handler.h b/include/multipass/sshfs_mount/sshfs_mount_handler.h index 71c0c211e63..fe483b62e67 100644 --- a/include/multipass/sshfs_mount/sshfs_mount_handler.h +++ b/include/multipass/sshfs_mount/sshfs_mount_handler.h @@ -28,8 +28,10 @@ namespace multipass class SSHFSMountHandler : public MountHandler { public: - SSHFSMountHandler(VirtualMachine* vm, const SSHKeyProvider* ssh_key_provider, const std::string& target, - const VMMount& mount); + SSHFSMountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + const std::string& target, + VMMount mount_spec); ~SSHFSMountHandler() override; void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; diff --git a/include/multipass/utils.h b/include/multipass/utils.h index 93674cfa324..a604f3af76b 100644 --- a/include/multipass/utils.h +++ b/include/multipass/utils.h @@ -101,8 +101,18 @@ bool valid_mac_address(const std::string& mac); // string helpers bool has_only_digits(const std::string& value); -std::string& trim_end( - std::string& s, std::function filter = [](char ch) { return std::isspace(ch); }); +template +Str&& trim_begin(Str&& s, Filter&& filter); +template +Str&& trim_begin(Str&& s); +template +Str&& trim_end(Str&& s, Filter&& filter); +template +Str&& trim_end(Str&& s); +template +Str&& trim(Str&& s, Filter&& filter); +template +Str&& trim(Str&& s); std::string& trim_newline(std::string& s); std::string escape_char(const std::string& s, char c); std::string escape_for_shell(const std::string& s); @@ -218,6 +228,9 @@ class Utils : public Singleton // virtual machine helpers virtual void wait_for_cloud_init(VirtualMachine* virtual_machine, std::chrono::milliseconds timeout, const SSHKeyProvider& key_provider) const; + virtual Path derive_instances_dir(const Path& data_dir, + const Path& backend_directory_name, + const Path& instances_subdir) const; // system info helpers virtual std::string get_kernel_version() const; @@ -230,6 +243,53 @@ class Utils : public Singleton }; } // namespace multipass +namespace multipass::utils::detail +{ +// see https://en.cppreference.com/w/cpp/string/byte/isspace#Notes +inline constexpr auto is_space = [](unsigned char c) { return std::isspace(c); }; +} // namespace multipass::utils::detail + +template +Str&& multipass::utils::trim_begin(Str&& s, Filter&& filter) +{ + const auto it = std::find_if_not(s.begin(), s.end(), std::forward(filter)); + s.erase(s.begin(), it); + return std::forward(s); +} + +template +Str&& multipass::utils::trim_begin(Str&& s) +{ + return trim_begin(std::forward(s), detail::is_space); +} + +template +Str&& multipass::utils::trim_end(Str&& s, Filter&& filter) +{ + auto rev_it = std::find_if_not(s.rbegin(), s.rend(), std::forward(filter)); + s.erase(rev_it.base(), s.end()); + return std::forward(s); +} + +template +Str&& multipass::utils::trim_end(Str&& s) +{ + return trim_end(std::forward(s), detail::is_space); +} + +template +Str&& multipass::utils::trim(Str&& s, Filter&& filter) +{ + auto&& ret = trim_end(std::forward(s), filter); + return trim_begin(std::forward(ret), std::forward(filter)); +} + +template +Str&& multipass::utils::trim(Str&& s) +{ + return trim(std::forward(s), detail::is_space); +} + template void multipass::utils::try_action_for(OnTimeoutCallable&& on_timeout, std::chrono::milliseconds timeout, TryAction&& try_action, Args&&... args) diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index fc9323496c6..baf3dec5027 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -20,6 +20,10 @@ #include "disabled_copy_move.h" #include "ip_address.h" +#include "path.h" + +#include +#include #include #include @@ -34,7 +38,9 @@ namespace multipass class MemorySize; class SSHKeyProvider; struct VMMount; +struct VMSpecs; class MountHandler; +class Snapshot; class VirtualMachine : private DisabledCopyMove { @@ -81,6 +87,28 @@ class VirtualMachine : private DisabledCopyMove const std::string& target, const VMMount& mount) = 0; + using SnapshotVista = std::vector>; // using vista to avoid confusion with C++ views + virtual SnapshotVista view_snapshots() const = 0; + virtual int get_num_snapshots() const noexcept = 0; + + virtual std::shared_ptr get_snapshot(const std::string& name) const = 0; + virtual std::shared_ptr get_snapshot(int index) const = 0; + virtual std::shared_ptr get_snapshot(const std::string& name) = 0; + virtual std::shared_ptr get_snapshot(int index) = 0; + + virtual std::shared_ptr take_snapshot(const VMSpecs& specs, + const std::string& snapshot_name, + const std::string& comment) = 0; + virtual void rename_snapshot(const std::string& old_name, + const std::string& new_name) = 0; // only VM can avoid repeated names + virtual void delete_snapshot(const std::string& name) = 0; + virtual void restore_snapshot(const std::string& name, VMSpecs& specs) = 0; + virtual void load_snapshots() = 0; + virtual std::vector get_childrens_names(const Snapshot* parent) const = 0; + virtual int get_snapshot_count() const = 0; + + QDir instance_directory() const; + VirtualMachine::State state; const std::string vm_name; std::condition_variable state_wait; @@ -89,8 +117,18 @@ class VirtualMachine : private DisabledCopyMove bool shutdown_while_starting{false}; protected: - VirtualMachine(VirtualMachine::State state, const std::string& vm_name) : state{state}, vm_name{vm_name} {}; - VirtualMachine(const std::string& vm_name) : VirtualMachine(State::off, vm_name){}; + const QDir instance_dir; + + VirtualMachine(VirtualMachine::State state, const std::string& vm_name, const Path& instance_dir) + : state{state}, vm_name{vm_name}, instance_dir{QDir{instance_dir}} {}; + VirtualMachine(const std::string& vm_name, const Path& instance_dir) + : VirtualMachine(State::off, vm_name, instance_dir){}; }; } // namespace multipass + +inline QDir multipass::VirtualMachine::instance_directory() const +{ + return instance_dir; // TODO this should probably only be known at the level of the base VM +} + #endif // MULTIPASS_VIRTUAL_MACHINE_H diff --git a/include/multipass/virtual_machine_factory.h b/include/multipass/virtual_machine_factory.h index 60380dff712..123e354d867 100644 --- a/include/multipass/virtual_machine_factory.h +++ b/include/multipass/virtual_machine_factory.h @@ -59,8 +59,9 @@ class VirtualMachineFactory : private DisabledCopyMove virtual VMImage prepare_source_image(const VMImage& source_image) = 0; virtual void prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) = 0; virtual void hypervisor_health_check() = 0; - virtual QString get_backend_directory_name() = 0; - virtual QString get_backend_version_string() = 0; + virtual QString get_backend_directory_name() const = 0; + virtual Path get_instance_directory(const std::string& name) const = 0; + virtual QString get_backend_version_string() const = 0; virtual VMImageVault::UPtr create_image_vault(std::vector image_hosts, URLDownloader* downloader, const Path& cache_dir_path, const Path& data_dir_path, const days& days_to_expire) = 0; diff --git a/include/multipass/vm_image_vault.h b/include/multipass/vm_image_vault.h index 6f31f54faea..1f145818429 100644 --- a/include/multipass/vm_image_vault.h +++ b/include/multipass/vm_image_vault.h @@ -80,9 +80,13 @@ class VMImageVault : private DisabledCopyMove using PrepareAction = std::function; virtual ~VMImageVault() = default; - virtual VMImage fetch_image(const FetchType& fetch_type, const Query& query, const PrepareAction& prepare, - const ProgressMonitor& monitor, const bool unlock, - const std::optional& checksum) = 0; + virtual VMImage fetch_image(const FetchType& fetch_type, + const Query& query, + const PrepareAction& prepare, + const ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const Path& save_dir) = 0; virtual void remove(const std::string& name) = 0; virtual bool has_record_for(const std::string& name) = 0; virtual void prune_expired_images() = 0; diff --git a/include/multipass/vm_mount.h b/include/multipass/vm_mount.h index 380051a6397..b3f6b37eff4 100644 --- a/include/multipass/vm_mount.h +++ b/include/multipass/vm_mount.h @@ -20,6 +20,8 @@ #include +#include + #include namespace multipass @@ -32,6 +34,12 @@ struct VMMount Native = 1 }; + VMMount() = default; + VMMount(const QJsonObject& json); + VMMount(const std::string& sourcePath, id_mappings gidMappings, id_mappings uidMappings, MountType mountType); + + QJsonObject serialize() const; + std::string source_path; id_mappings gid_mappings; id_mappings uid_mappings; @@ -43,6 +51,12 @@ inline bool operator==(const VMMount& a, const VMMount& b) return std::tie(a.source_path, a.gid_mappings, a.uid_mappings) == std::tie(b.source_path, b.gid_mappings, b.uid_mappings); } + +inline bool operator!=(const VMMount& a, const VMMount& b) // TODO drop in C++20 +{ + return !(a == b); +} + } // namespace multipass namespace fmt diff --git a/src/daemon/vm_specs.h b/include/multipass/vm_specs.h similarity index 55% rename from src/daemon/vm_specs.h rename to include/multipass/vm_specs.h index 0fc46851785..42af152bed6 100644 --- a/src/daemon/vm_specs.h +++ b/include/multipass/vm_specs.h @@ -18,10 +18,10 @@ #ifndef MULTIPASS_VM_SPECS_H #define MULTIPASS_VM_SPECS_H -#include -#include -#include -#include +#include "memory_size.h" +#include "network_interface.h" +#include "virtual_machine.h" +#include "vm_mount.h" #include #include @@ -48,11 +48,32 @@ struct VMSpecs inline bool operator==(const VMSpecs& a, const VMSpecs& b) { - return std::tie(a.num_cores, a.mem_size, a.disk_space, a.default_mac_address, a.extra_interfaces, a.ssh_username, - a.state, a.mounts, a.deleted, a.metadata) == - std::tie(b.num_cores, b.mem_size, b.disk_space, b.default_mac_address, b.extra_interfaces, b.ssh_username, - b.state, b.mounts, b.deleted, b.metadata); + return std::tie(a.num_cores, + a.mem_size, + a.disk_space, + a.default_mac_address, + a.extra_interfaces, + a.ssh_username, + a.state, + a.mounts, + a.deleted, + a.metadata) == std::tie(b.num_cores, + b.mem_size, + b.disk_space, + b.default_mac_address, + b.extra_interfaces, + b.ssh_username, + b.state, + b.mounts, + b.deleted, + b.metadata); } + +inline bool operator!=(const VMSpecs& a, const VMSpecs& b) // TODO drop in C++20 +{ + return !(a == b); +} + } // namespace multipass #endif // MULTIPASS_VM_SPECS_H diff --git a/src/client/cli/client.cpp b/src/client/cli/client.cpp index 59b52a2df88..9de747ff5b3 100644 --- a/src/client/cli/client.cpp +++ b/src/client/cli/client.cpp @@ -34,8 +34,10 @@ #include "cmd/recover.h" #include "cmd/remote_settings_handler.h" #include "cmd/restart.h" +#include "cmd/restore.h" #include "cmd/set.h" #include "cmd/shell.h" +#include "cmd/snapshot.h" #include "cmd/start.h" #include "cmd/stop.h" #include "cmd/suspend.h" @@ -93,8 +95,10 @@ mp::Client::Client(ClientConfig& config) add_command(); add_command(aliases); add_command(); + add_command(); add_command(); add_command(); + add_command(); add_command(); add_command(); add_command(); diff --git a/src/client/cli/cmd/CMakeLists.txt b/src/client/cli/cmd/CMakeLists.txt index 873baf98506..d5606f61535 100644 --- a/src/client/cli/cmd/CMakeLists.txt +++ b/src/client/cli/cmd/CMakeLists.txt @@ -34,8 +34,10 @@ add_library(commands STATIC recover.cpp remote_settings_handler.cpp restart.cpp + restore.cpp set.cpp shell.cpp + snapshot.cpp start.cpp stop.cpp suspend.cpp diff --git a/src/client/cli/cmd/alias.cpp b/src/client/cli/cmd/alias.cpp index 081d407999f..814a6043c94 100644 --- a/src/client/cli/cmd/alias.cpp +++ b/src/client/cli/cmd/alias.cpp @@ -116,7 +116,7 @@ mp::ParseCode cmd::Alias::parse_args(mp::ArgParser* parser) auto instance = definition.left(colon_pos).toStdString(); auto working_directory = parser->isSet(no_alias_dir_mapping_option) ? "default" : "map"; - info_request.mutable_instance_names()->add_instance_name(instance); + info_request.add_instance_snapshot_pairs()->set_instance_name(instance); info_request.set_verbosity_level(0); info_request.set_no_runtime_information(true); diff --git a/src/client/cli/cmd/common_callbacks.h b/src/client/cli/cmd/common_callbacks.h index f0f3090086d..02f24858d60 100644 --- a/src/client/cli/cmd/common_callbacks.h +++ b/src/client/cli/cmd/common_callbacks.h @@ -31,8 +31,21 @@ auto make_logging_spinner_callback(AnimatedSpinner& spinner, std::ostream& strea { return [&spinner, &stream](const Reply& reply, grpc::ClientReaderWriterInterface*) { if (!reply.log_line().empty()) - { spinner.print(stream, reply.log_line()); + }; +} + +template +auto make_reply_spinner_callback(AnimatedSpinner& spinner, std::ostream& stream) +{ + return [&spinner, &stream](const Reply& reply, grpc::ClientReaderWriterInterface*) { + if (!reply.log_line().empty()) + spinner.print(stream, reply.log_line()); + + if (const auto& msg = reply.reply_message(); !msg.empty()) + { + spinner.stop(); + spinner.start(msg); } }; } @@ -42,9 +55,7 @@ auto make_iterative_spinner_callback(AnimatedSpinner& spinner, Terminal& term) { return [&spinner, &term](const Reply& reply, grpc::ClientReaderWriterInterface* client) { if (!reply.log_line().empty()) - { spinner.print(term.cerr(), reply.log_line()); - } if (reply.password_requested()) { diff --git a/src/client/cli/cmd/common_cli.cpp b/src/client/cli/cmd/common_cli.cpp index 8cf1a27de5b..7c260d26f9d 100644 --- a/src/client/cli/cmd/common_cli.cpp +++ b/src/client/cli/cmd/common_cli.cpp @@ -76,6 +76,25 @@ mp::InstanceNames cmd::add_instance_names(const mp::ArgParser* parser, const std return instance_names; } +std::vector cmd::add_instance_and_snapshot_names(const mp::ArgParser* parser) +{ + std::vector instance_snapshot_names; + instance_snapshot_names.reserve(parser->positionalArguments().count()); + + for (const auto& arg : parser->positionalArguments()) + { + mp::InstanceSnapshotPair inst_snap_name; + auto index = arg.indexOf('.'); + inst_snap_name.set_instance_name(arg.left(index).toStdString()); + if (index >= 0) + inst_snap_name.set_snapshot_name(arg.right(arg.length() - index - 1).toStdString()); + + instance_snapshot_names.push_back(inst_snap_name); + } + + return instance_snapshot_names; +} + mp::ParseCode cmd::handle_format_option(const mp::ArgParser* parser, mp::Formatter** chosen_formatter, std::ostream& cerr) { diff --git a/src/client/cli/cmd/common_cli.h b/src/client/cli/cmd/common_cli.h index bd5eeea68f5..2376471e1cc 100644 --- a/src/client/cli/cmd/common_cli.h +++ b/src/client/cli/cmd/common_cli.h @@ -26,6 +26,8 @@ #include +#include + using RpcMethod = multipass::Rpc::StubInterface; namespace multipass @@ -39,10 +41,13 @@ namespace cmd { const QString all_option_name{"all"}; const QString format_option_name{"format"}; +const std::regex yes_answer{"y|yes", std::regex::icase | std::regex::optimize}; +const std::regex no_answer{"n|no", std::regex::icase | std::regex::optimize}; ParseCode check_for_name_and_all_option_conflict(const ArgParser* parser, std::ostream& cerr, bool allow_empty = false); InstanceNames add_instance_names(const ArgParser* parser); InstanceNames add_instance_names(const ArgParser* parser, const std::string& default_name); +std::vector add_instance_and_snapshot_names(const ArgParser* parser); ParseCode handle_format_option(const ArgParser* parser, Formatter** chosen_formatter, std::ostream& cerr); std::string instance_action_message_for(const InstanceNames& instance_names, const std::string& action_name); ReturnCode run_cmd(const QStringList& args, const ArgParser* parser, std::ostream& cout, std::ostream& cerr); diff --git a/src/client/cli/cmd/delete.cpp b/src/client/cli/cmd/delete.cpp index dd8ce3b7af2..1077f995295 100644 --- a/src/client/cli/cmd/delete.cpp +++ b/src/client/cli/cmd/delete.cpp @@ -24,6 +24,13 @@ namespace mp = multipass; namespace cmd = multipass::cmd; +namespace +{ +constexpr auto no_purge_base_error_msg = + "Snapshots can only be purged (after deletion, they cannot be recovered). Please use the `--purge` " + "flag if that is what you want"; +} + mp::ReturnCode cmd::Delete::run(mp::ArgParser* parser) { auto ret = parse_args(parser); @@ -75,38 +82,83 @@ std::string cmd::Delete::name() const QString cmd::Delete::short_help() const { - return QStringLiteral("Delete instances"); + return QStringLiteral("Delete instances and snapshots"); } QString cmd::Delete::description() const { - return QStringLiteral("Delete instances, to be purged with the \"purge\" command,\n" - "or recovered with the \"recover\" command."); + return QStringLiteral( + "Delete instances and snapshots. Instances can be purged immediately or later on,\n" + "with the \"purge\" command. Until they are purged, instances can be recovered\n" + "with the \"recover\" command. Snapshots cannot be recovered after deletion and must be purged at once."); } mp::ParseCode cmd::Delete::parse_args(mp::ArgParser* parser) { - parser->addPositionalArgument("name", "Names of instances to delete", " [ ...]"); + parser->addPositionalArgument("name", + "Names of instances and snapshots to delete", + "[.snapshot] [[.snapshot] ...]"); - QCommandLineOption all_option(all_option_name, "Delete all instances"); - parser->addOption(all_option); - - QCommandLineOption purge_option({"p", "purge"}, "Purge instances immediately"); - parser->addOption(purge_option); + QCommandLineOption all_option(all_option_name, "Delete all instances and snapshots"); + QCommandLineOption purge_option({"p", "purge"}, "Permanently delete specified instances and snapshots immediately"); + parser->addOptions({all_option, purge_option}); auto status = parser->commandParse(this); if (status != ParseCode::Ok) return status; - auto parse_code = check_for_name_and_all_option_conflict(parser, cerr); - if (parse_code != ParseCode::Ok) - return parse_code; + status = check_for_name_and_all_option_conflict(parser, cerr); + if (status != ParseCode::Ok) + return status; + + request.set_purge(parser->isSet(purge_option)); + + status = parse_instances_snapshots(parser); + + return status; +} + +mp::ParseCode cmd::Delete::parse_instances_snapshots(mp::ArgParser* parser) +{ + bool instance_found = false, snapshot_found = false; + std::string instances, snapshots; + for (const auto& item : cmd::add_instance_and_snapshot_names(parser)) + { + if (!item.has_snapshot_name()) + { + instances.append(fmt::format("{} ", item.instance_name())); + instance_found = true; + } + else + { + snapshots.append(fmt::format("{}.{} ", item.instance_name(), item.snapshot_name())); + snapshot_found = true; + } - request.mutable_instance_names()->CopyFrom(add_instance_names(parser)); + request.add_instance_snapshot_pairs()->CopyFrom(item); + } + + return enforce_purged_snapshots(instances, snapshots, instance_found, snapshot_found); +} - if (parser->isSet(purge_option)) +mp::ParseCode cmd::Delete::enforce_purged_snapshots(std::string& instances, + std::string& snapshots, + bool instance_found, + bool snapshot_found) +{ + if (snapshot_found && !request.purge()) { - request.set_purge(true); + if (instance_found) + cerr << fmt::format("{}:\n\n\tmultipass delete --purge {}\n\nYou can use a separate command to delete " + "instances without purging them:\n\n\tmultipass delete {}\n", + no_purge_base_error_msg, + snapshots, + instances); + else + cerr << fmt::format("{}.\n", no_purge_base_error_msg); + + return mp::ParseCode::CommandLineError; } - return status; + + return mp::ParseCode::Ok; } diff --git a/src/client/cli/cmd/delete.h b/src/client/cli/cmd/delete.h index 058820191a6..3b33d98d066 100644 --- a/src/client/cli/cmd/delete.h +++ b/src/client/cli/cmd/delete.h @@ -45,6 +45,11 @@ class Delete final : public Command DeleteRequest request; ParseCode parse_args(ArgParser* parser); + ParseCode parse_instances_snapshots(ArgParser* parser); + ParseCode enforce_purged_snapshots(std::string& instances, + std::string& snapshots, + bool instance_found, + bool snapshot_found); }; } // namespace cmd } // namespace multipass diff --git a/src/client/cli/cmd/exec.cpp b/src/client/cli/cmd/exec.cpp index 7542e147222..497c26126eb 100644 --- a/src/client/cli/cmd/exec.cpp +++ b/src/client/cli/cmd/exec.cpp @@ -78,7 +78,7 @@ mp::ReturnCode cmd::Exec::run(mp::ArgParser* parser) QStringList split_exec_dir = clean_exec_dir.split('/'); auto on_info_success = [&work_dir, &split_exec_dir](mp::InfoReply& reply) { - for (const auto& mount : reply.info(0).mount_info().mount_paths()) + for (const auto& mount : reply.details(0).mount_info().mount_paths()) { auto source_dir = QDir(QString::fromStdString(mount.source_path())); auto clean_source_dir = QDir::cleanPath(source_dir.absolutePath()); @@ -102,10 +102,7 @@ mp::ReturnCode cmd::Exec::run(mp::ArgParser* parser) info_request.set_verbosity_level(parser->verbosityLevel()); - InstanceNames instance_names; - auto info_instance_name = instance_names.add_instance_name(); - info_instance_name->append(instance_name); - info_request.mutable_instance_names()->CopyFrom(instance_names); + info_request.add_instance_snapshot_pairs()->set_instance_name(instance_name); info_request.set_no_runtime_information(true); dispatch(&RpcMethod::info, info_request, on_info_success, on_info_failure); diff --git a/src/client/cli/cmd/info.cpp b/src/client/cli/cmd/info.cpp index 5274f2b73fd..5594d30fee0 100644 --- a/src/client/cli/cmd/info.cpp +++ b/src/client/cli/cmd/info.cpp @@ -48,47 +48,73 @@ std::string cmd::Info::name() const { return "info"; } QString cmd::Info::short_help() const { - return QStringLiteral("Display information about instances"); + return QStringLiteral("Display information about instances or snapshots"); } QString cmd::Info::description() const { - return QStringLiteral("Display information about instances"); + return short_help(); } mp::ParseCode cmd::Info::parse_args(mp::ArgParser* parser) { - parser->addPositionalArgument("name", "Names of instances to display information about", " [ ...]"); - - QCommandLineOption all_option(all_option_name, "Display info for all instances"); - parser->addOption(all_option); + parser->addPositionalArgument("instance/snapshot", + "Names of instances or snapshots to display information about", + "[.snapshot] [[.snapshot] ...]"); + QCommandLineOption all_option(all_option_name, "Display info for all instances."); + all_option.setFlags(QCommandLineOption::HiddenFromHelp); QCommandLineOption noRuntimeInfoOption( "no-runtime-information", - "Retrieve from the daemon only the information obtained without running commands on the instance"); + "Retrieve from the daemon only the information obtained without running commands on the instance."); noRuntimeInfoOption.setFlags(QCommandLineOption::HiddenFromHelp); - parser->addOption(noRuntimeInfoOption); - - QCommandLineOption formatOption( - "format", "Output info in the requested format.\nValid formats are: table (default), json, csv and yaml", - "format", "table"); - parser->addOption(formatOption); + QCommandLineOption snapshots_option{"snapshots", + "Display detailed information about the snapshots of specified instances. This " + "option has no effect on snapshot arguments. Omit instance/snapshot arguments " + "to obtain detailed information on all the snapshots of all instances."}; + QCommandLineOption format_option( + format_option_name, + "Output info in the requested format.\nValid formats are: table (default), json, csv and yaml.", + format_option_name, + "table"); + + parser->addOptions({all_option, noRuntimeInfoOption, snapshots_option, format_option}); auto status = parser->commandParse(this); + if (status != ParseCode::Ok) + return status; + status = handle_format_option(parser, &chosen_formatter, cerr); + + status = check_for_name_and_all_option_conflict(parser, cerr, true); if (status != ParseCode::Ok) - { return status; - } - auto parse_code = check_for_name_and_all_option_conflict(parser, cerr); - if (parse_code != ParseCode::Ok) - return parse_code; + if (parser->isSet(all_option_name)) + cerr << "Warning: the `--all` flag for the `info` command is deprecated. Please use `info` with no positional " + "arguments for the same effect.\n"; + + bool instance_found = false, snapshot_found = false; + for (const auto& item : add_instance_and_snapshot_names(parser)) + { + if (!item.has_snapshot_name()) + instance_found = true; + else + snapshot_found = true; + + request.add_instance_snapshot_pairs()->CopyFrom(item); + } - request.mutable_instance_names()->CopyFrom(add_instance_names(parser)); request.set_no_runtime_information(parser->isSet(noRuntimeInfoOption)); - status = handle_format_option(parser, &chosen_formatter, cerr); + const auto& snapshots_only = parser->isSet(snapshots_option); + request.set_snapshots(snapshots_only); + + if (instance_found && snapshot_found && parser->value(format_option_name) == "csv" && !snapshots_only) + { + cerr << "Mixed snapshot and instance arguments are not supported with CSV format\n"; + return ParseCode::CommandLineError; + } return status; } diff --git a/src/client/cli/cmd/launch.cpp b/src/client/cli/cmd/launch.cpp index 6d8a76f2813..1b0e9413674 100644 --- a/src/client/cli/cmd/launch.cpp +++ b/src/client/cli/cmd/launch.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include namespace mp = multipass; @@ -53,9 +52,6 @@ namespace fs = std::filesystem; namespace { -const std::regex yes{"y|yes", std::regex::icase | std::regex::optimize}; -const std::regex no{"n|no", std::regex::icase | std::regex::optimize}; - constexpr bool on_windows() { // TODO when we have remote client-daemon communication, we need to get the daemon's platform return @@ -318,7 +314,7 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser) if (!conversion_pass || cpu_count < 1) { - fmt::print(cerr, "error: Invalid CPU count '{}', need a positive integer value.\n", cpu_text); + fmt::print(cerr, "Error: invalid CPU count '{}', need a positive integer value.\n", cpu_text); return ParseCode::CommandLineError; } @@ -329,14 +325,14 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser) { if (parser->isSet(memOption) && parser->isSet(memOptionDeprecated)) { - cerr << "error: Invalid option(s) used for memory allocation. Please use \"--memory\" to specify " - "amount of memory to allocate.\n"; + cerr << "Error: invalid option(s) used for memory allocation. Please use \"--memory\" to specify amount of " + "memory to allocate.\n"; return ParseCode::CommandLineError; } if (parser->isSet(memOptionDeprecated)) - cout << "warning: \"--mem\" long option will be deprecated in favour of \"--memory\" in a future release. " - "Please update any scripts, etc.\n"; + cerr << "Warning: the \"--mem\" long option is deprecated in favour of \"--memory\". Please update any " + "scripts, etc.\n"; auto arg_mem_size = parser->isSet(memOption) ? parser->value(memOption).toStdString() : parser->value(memOptionDeprecated).toStdString(); @@ -484,7 +480,8 @@ mp::ReturnCode cmd::Launch::request_launch(const ArgParser* parser) } if (warning_aliases.size()) - cout << fmt::format("Warning: unable to create {} {}.\n", warning_aliases.size() == 1 ? "alias" : "aliases", + cerr << fmt::format("Warning: unable to create {} {}.\n", + warning_aliases.size() == 1 ? "alias" : "aliases", fmt::join(warning_aliases, ", ")); for (const auto& workspace_to_be_created : reply.workspaces_to_be_created()) @@ -640,9 +637,9 @@ bool cmd::Launch::ask_bridge_permission(multipass::LaunchReply& reply) { std::string answer; std::getline(term->cin(), answer); - if (std::regex_match(answer, yes)) + if (std::regex_match(answer, yes_answer)) return true; - else if (std::regex_match(answer, no)) + else if (std::regex_match(answer, no_answer)) return false; else cout << "Please answer yes/no: "; diff --git a/src/client/cli/cmd/list.cpp b/src/client/cli/cmd/list.cpp index 93c0ea13ca8..8c237a68b48 100644 --- a/src/client/cli/cmd/list.cpp +++ b/src/client/cli/cmd/list.cpp @@ -59,24 +59,24 @@ std::vector cmd::List::aliases() const QString cmd::List::short_help() const { - return QStringLiteral("List all available instances"); + return QStringLiteral("List all available instances or snapshots"); } QString cmd::List::description() const { - return QStringLiteral("List all instances which have been created."); + return QStringLiteral("List all instances or snapshots which have been created."); } mp::ParseCode cmd::List::parse_args(mp::ArgParser* parser) { + QCommandLineOption snapshotsOption("snapshots", "List all available snapshots"); QCommandLineOption formatOption( "format", "Output list in the requested format.\nValid formats are: table (default), json, csv and yaml", "format", "table"); - QCommandLineOption noIpv4Option("no-ipv4", "Do not query the instances for the IPv4's they are using"); noIpv4Option.setFlags(QCommandLineOption::HiddenFromHelp); - parser->addOptions({formatOption, noIpv4Option}); + parser->addOptions({snapshotsOption, formatOption, noIpv4Option}); auto status = parser->commandParse(this); @@ -91,6 +91,13 @@ mp::ParseCode cmd::List::parse_args(mp::ArgParser* parser) return ParseCode::CommandLineError; } + if (parser->isSet(snapshotsOption) && parser->isSet(noIpv4Option)) + { + cerr << "IP addresses are not applicable in conjunction with listing snapshots\n"; + return ParseCode::CommandLineError; + } + + request.set_snapshots(parser->isSet(snapshotsOption)); request.set_request_ipv4(!parser->isSet(noIpv4Option)); status = handle_format_option(parser, &chosen_formatter, cerr); diff --git a/src/client/cli/cmd/remote_settings_handler.cpp b/src/client/cli/cmd/remote_settings_handler.cpp index 77158e34012..0676ef454c1 100644 --- a/src/client/cli/cmd/remote_settings_handler.cpp +++ b/src/client/cli/cmd/remote_settings_handler.cpp @@ -17,6 +17,7 @@ #include "remote_settings_handler.h" #include "animated_spinner.h" +#include "common_callbacks.h" #include #include @@ -119,21 +120,13 @@ class RemoteSet : public RemoteSettingsCmd set_request.set_val(val.toStdString()); mp::AnimatedSpinner spinner{cout}; - auto streaming_callback = [this, - &spinner](mp::SetReply& reply, - grpc::ClientReaderWriterInterface* client) { - if (const auto& msg = reply.log_line(); !msg.empty()) - spinner.print(cerr, reply.log_line()); - - if (const auto& msg = reply.reply_message(); !msg.empty()) - { - spinner.stop(); - spinner.start(msg); - } - }; [[maybe_unused]] auto ret = - dispatch(&RpcMethod::set, set_request, on_success, on_failure, streaming_callback); + dispatch(&RpcMethod::set, + set_request, + on_success, + on_failure, + mp::make_reply_spinner_callback(spinner, cerr)); assert(ret == mp::ReturnCode::Ok && "should have thrown otherwise"); } }; diff --git a/src/client/cli/cmd/restore.cpp b/src/client/cli/cmd/restore.cpp new file mode 100644 index 00000000000..557987b7b48 --- /dev/null +++ b/src/client/cli/cmd/restore.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "restore.h" +#include "animated_spinner.h" +#include "common_callbacks.h" +#include "common_cli.h" + +#include +#include +#include + +namespace mp = multipass; +namespace cmd = multipass::cmd; + +mp::ReturnCode cmd::Restore::run(mp::ArgParser* parser) +{ + if (auto ret = parse_args(parser); ret != ParseCode::Ok) + return parser->returnCodeFrom(ret); + + AnimatedSpinner spinner{cout}; + + auto on_success = [this, &spinner](mp::RestoreReply& reply) { + spinner.stop(); + fmt::print(cout, "Snapshot restored: {}.{}\n", request.instance(), request.snapshot()); + return ReturnCode::Ok; + }; + + auto on_failure = [this, &spinner](grpc::Status& status) { + spinner.stop(); + return standard_failure_handler_for(name(), cerr, status); + }; + + using Client = grpc::ClientReaderWriterInterface; + auto streaming_callback = [this, &spinner](mp::RestoreReply& reply, Client* client) { + if (!reply.log_line().empty()) + spinner.print(cerr, reply.log_line()); + + if (const auto& msg = reply.reply_message(); !msg.empty()) + { + spinner.stop(); + spinner.start(msg); + } + + if (reply.confirm_destructive()) + { + spinner.stop(); + RestoreRequest client_response; + + if (term->is_live()) + client_response.set_destructive(confirm_destruction(request.instance())); + else + throw std::runtime_error("Unable to query client for confirmation. Use '--destructive' to " + "automatically discard current machine state."); + + client->Write(client_response); + spinner.start(); + } + }; + + return dispatch(&RpcMethod::restore, request, on_success, on_failure, streaming_callback); +} + +std::string cmd::Restore::name() const +{ + return "restore"; +} + +QString cmd::Restore::short_help() const +{ + return QStringLiteral("Restore an instance from a snapshot"); +} + +QString cmd::Restore::description() const +{ + return QStringLiteral("Restore an instance to the state of a previously taken snapshot."); +} + +mp::ParseCode cmd::Restore::parse_args(mp::ArgParser* parser) +{ + parser->addPositionalArgument("instance.snapshot", + "The instance to restore and snapshot to use, in . format, where " + " is the name of an instance, and is the name of a snapshot", + "."); + + QCommandLineOption destructive({"d", "destructive"}, "Discard the current state of the instance"); + parser->addOption(destructive); + + auto status = parser->commandParse(this); + if (status != ParseCode::Ok) + return status; + + const auto positional_args = parser->positionalArguments(); + const auto num_args = positional_args.count(); + if (num_args < 1) + { + cerr << "Need the name of an instance and snapshot to restore.\n"; + return ParseCode::CommandLineError; + } + + if (num_args > 1) + { + cerr << "Too many arguments supplied.\n"; + return ParseCode::CommandLineError; + } + + const auto tokens = parser->positionalArguments().at(0).split('.'); + if (tokens.size() != 2 || tokens[0].isEmpty() || tokens[1].isEmpty()) + { + cerr << "Invalid format. Please specify the instance to restore and snapshot to use in the form " + "..\n"; + return ParseCode::CommandLineError; + } + + request.set_instance(tokens[0].toStdString()); + request.set_snapshot(tokens[1].toStdString()); + request.set_destructive(parser->isSet(destructive)); + request.set_verbosity_level(parser->verbosityLevel()); + + return ParseCode::Ok; +} + +bool cmd::Restore::confirm_destruction(const std::string& instance_name) +{ + static constexpr auto prompt_text = + "Do you want to take a snapshot of {} before discarding its current state? (Yes/no)"; + static constexpr auto invalid_input = "Please answer Yes/no"; + mp::PlainPrompter prompter(term); + + auto answer = prompter.prompt(fmt::format(prompt_text, instance_name)); + while (!answer.empty() && !std::regex_match(answer, yes_answer) && !std::regex_match(answer, no_answer)) + answer = prompter.prompt(invalid_input); + + return std::regex_match(answer, no_answer); +} diff --git a/src/client/cli/cmd/restore.h b/src/client/cli/cmd/restore.h new file mode 100644 index 00000000000..a2ab3b763ae --- /dev/null +++ b/src/client/cli/cmd/restore.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_RESTORE_H +#define MULTIPASS_RESTORE_H + +#include + +namespace multipass::cmd +{ +class Restore : public Command +{ +public: + using Command::Command; + ReturnCode run(ArgParser* parser) override; + + std::string name() const override; + QString short_help() const override; + QString description() const override; + +private: + ParseCode parse_args(ArgParser* parser); + bool confirm_destruction(const std::string& instance_name); + RestoreRequest request; +}; +} // namespace multipass::cmd + +#endif // MULTIPASS_RESTORE_H diff --git a/src/client/cli/cmd/snapshot.cpp b/src/client/cli/cmd/snapshot.cpp new file mode 100644 index 00000000000..486ce277053 --- /dev/null +++ b/src/client/cli/cmd/snapshot.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "snapshot.h" + +#include "animated_spinner.h" +#include "common_callbacks.h" +#include "common_cli.h" + +#include + +namespace mp = multipass; +namespace cmd = mp::cmd; + +mp::ReturnCode cmd::Snapshot::run(mp::ArgParser* parser) +{ + if (auto ret = parse_args(parser); ret != ParseCode::Ok) + return parser->returnCodeFrom(ret); + + AnimatedSpinner spinner{cout}; + + auto on_success = [this, &spinner](mp::SnapshotReply& reply) { + spinner.stop(); + fmt::print(cout, "Snapshot taken: {}.{}\n", request.instance(), reply.snapshot()); + return ReturnCode::Ok; + }; + + auto on_failure = [this, &spinner](grpc::Status& status) { + spinner.stop(); + return standard_failure_handler_for(name(), cerr, status); + }; + + spinner.start("Taking snapshot"); + return dispatch(&RpcMethod::snapshot, + request, + on_success, + on_failure, + make_logging_spinner_callback(spinner, cerr)); +} + +std::string cmd::Snapshot::name() const +{ + return "snapshot"; +} + +QString cmd::Snapshot::short_help() const +{ + return QStringLiteral("Take a snapshot of an instance"); +} + +QString cmd::Snapshot::description() const +{ + return QStringLiteral("Take a snapshot of an instance that can later be restored to recover the current state."); +} + +mp::ParseCode cmd::Snapshot::parse_args(mp::ArgParser* parser) +{ + parser->addPositionalArgument("instance", "The instance to take a snapshot of."); + QCommandLineOption name_opt({"n", "name"}, + "An optional name for the snapshot, subject to the same validity rules as instance " + "names (see `help launch`). Default: \"snapshotN\", where N is one plus the " + "number of snapshots that were ever taken for .", + "name"); + QCommandLineOption comment_opt{{"comment", "c", "m"}, + "An optional free comment to associate with the snapshot. (Hint: quote the text to " + "avoid spaces being parsed by your shell)", + "comment"}; + parser->addOptions({name_opt, comment_opt}); + + if (auto status = parser->commandParse(this); status != ParseCode::Ok) + return status; + + const auto positional_args = parser->positionalArguments(); + const auto num_args = positional_args.count(); + if (num_args < 1) + { + cerr << "Need the name of an instance to snapshot.\n"; + return ParseCode::CommandLineError; + } + + if (num_args > 1) + { + cerr << "Too many arguments supplied\n"; + return ParseCode::CommandLineError; + } + + request.set_instance(positional_args.first().toStdString()); + request.set_comment(parser->value(comment_opt).toStdString()); + request.set_snapshot(parser->value(name_opt).toStdString()); + request.set_verbosity_level(parser->verbosityLevel()); + + return ParseCode::Ok; +} diff --git a/src/client/cli/cmd/snapshot.h b/src/client/cli/cmd/snapshot.h new file mode 100644 index 00000000000..e51ae7b8e69 --- /dev/null +++ b/src/client/cli/cmd/snapshot.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_SNAPSHOT_H +#define MULTIPASS_SNAPSHOT_H + +#include + +namespace multipass::cmd +{ +class Snapshot : public Command +{ +public: + using Command::Command; + ReturnCode run(ArgParser* parser) override; + + std::string name() const override; + QString short_help() const override; + QString description() const override; + +private: + ParseCode parse_args(ArgParser* parser); + SnapshotRequest request; +}; +} // namespace multipass::cmd + +#endif // MULTIPASS_SNAPSHOT_H diff --git a/src/client/cli/formatter/csv_formatter.cpp b/src/client/cli/formatter/csv_formatter.cpp index a79e1916591..dbf17c86b71 100644 --- a/src/client/cli/formatter/csv_formatter.cpp +++ b/src/client/cli/formatter/csv_formatter.cpp @@ -45,41 +45,104 @@ std::string format_images(const google::protobuf::RepeatedPtrField {};", mount->source_path(), mount->target_path()); + fmt::format_to(std::back_inserter(buf), "{} => {}", mount->source_path(), mount->target_path()); + + return fmt::to_string(buf); +} + +std::string generate_snapshot_details(const mp::InfoReply reply) +{ + fmt::memory_buffer buf; + + fmt::format_to(std::back_inserter(buf), + "Snapshot,Instance,CPU(s),Disk space,Memory size,Mounts,Created,Parent,Children,Comment\n"); + + for (const auto& info : mp::format::sorted(reply.details())) + { + const auto& fundamentals = info.snapshot_info().fundamentals(); + + fmt::format_to(std::back_inserter(buf), + "{},{},{},{},{},", + fundamentals.snapshot_name(), + info.name(), + info.cpu_count(), + info.disk_total(), + info.memory_total()); + + fmt::format_to(std::back_inserter(buf), format_mounts(info.mount_info())); + + fmt::format_to(std::back_inserter(buf), + ",{},{},{},\"{}\"\n", + google::protobuf::util::TimeUtil::ToString(fundamentals.creation_timestamp()), + fundamentals.parent(), + fmt::join(info.snapshot_info().children(), ";"), + fundamentals.comment()); + } + + return fmt::to_string(buf); +} + +std::string generate_instance_details(const mp::InfoReply reply) +{ + fmt::memory_buffer buf; + fmt::format_to( std::back_inserter(buf), "Name,State,Ipv4,Ipv6,Release,Image hash,Image release,Load,Disk usage,Disk total,Memory usage,Memory " - "total,Mounts,AllIPv4,CPU(s)\n"); + "total,Mounts,AllIPv4,CPU(s),Snapshots\n"); - for (const auto& info : format::sorted(reply.info())) + for (const auto& info : mp::format::sorted(reply.details())) { - fmt::format_to(std::back_inserter(buf), "{},{},{},{},{},{},{},{},{},{},{},{},", info.name(), - mp::format::status_string_for(info.instance_status()), info.ipv4_size() ? info.ipv4(0) : "", - info.ipv6_size() ? info.ipv6(0) : "", info.current_release(), info.id(), info.image_release(), - info.load(), info.disk_usage(), info.disk_total(), info.memory_usage(), info.memory_total()); + assert(info.has_instance_info() && + "outputting instance and snapshot details together is not supported in csv format"); + const auto& instance_details = info.instance_info(); - auto mount_paths = info.mount_info().mount_paths(); - for (auto mount = mount_paths.cbegin(); mount != mount_paths.cend(); ++mount) - { - fmt::format_to(std::back_inserter(buf), "{} => {};", mount->source_path(), mount->target_path()); - } + fmt::format_to(std::back_inserter(buf), + "{},{},{},{},{},{},{},{},{},{},{},{},", + info.name(), + mp::format::status_string_for(info.instance_status()), + instance_details.ipv4_size() ? instance_details.ipv4(0) : "", + instance_details.ipv6_size() ? instance_details.ipv6(0) : "", + instance_details.current_release(), + instance_details.id(), + instance_details.image_release(), + instance_details.load(), + instance_details.disk_usage(), + info.disk_total(), + instance_details.memory_usage(), + info.memory_total()); - fmt::format_to(std::back_inserter(buf), ",\"{}\";,{}\n", fmt::join(info.ipv4(), ","), info.cpu_count()); + fmt::format_to(std::back_inserter(buf), format_mounts(info.mount_info())); + + fmt::format_to(std::back_inserter(buf), + ",{},{},{}\n", + fmt::join(instance_details.ipv4(), ";"), + info.cpu_count(), + instance_details.num_snapshots()); } + return fmt::to_string(buf); } -std::string mp::CSVFormatter::format(const ListReply& reply) const +std::string generate_instances_list(const mp::InstancesList& instance_list) { fmt::memory_buffer buf; fmt::format_to(std::back_inserter(buf), "Name,State,IPv4,IPv6,Release,AllIPv4\n"); - for (const auto& instance : format::sorted(reply.instances())) + for (const auto& instance : mp::format::sorted(instance_list.instances())) { fmt::format_to(std::back_inserter(buf), "{},{},{},{},{},\"{}\"\n", instance.name(), mp::format::status_string_for(instance.instance_status()), @@ -92,6 +155,59 @@ std::string mp::CSVFormatter::format(const ListReply& reply) const return fmt::to_string(buf); } +std::string generate_snapshots_list(const mp::SnapshotsList& snapshot_list) +{ + fmt::memory_buffer buf; + + fmt::format_to(std::back_inserter(buf), "Instance,Snapshot,Parent,Comment\n"); + + for (const auto& item : mp::format::sorted(snapshot_list.snapshots())) + { + const auto& snapshot = item.fundamentals(); + fmt::format_to(std::back_inserter(buf), + "{},{},{},\"{}\"\n", + item.name(), + snapshot.snapshot_name(), + snapshot.parent(), + snapshot.comment()); + } + + return fmt::to_string(buf); +} +} // namespace + +std::string mp::CSVFormatter::format(const InfoReply& reply) const +{ + std::string output; + + if (reply.details_size() > 0) + { + if (reply.details()[0].has_instance_info()) + output = generate_instance_details(reply); + else + output = generate_snapshot_details(reply); + } + + return output; +} + +std::string mp::CSVFormatter::format(const ListReply& reply) const +{ + std::string output; + + if (reply.has_instance_list()) + { + output = generate_instances_list(reply.instance_list()); + } + else + { + assert(reply.has_snapshot_list() && "either one of instances or snapshots should be populated"); + output = generate_snapshots_list(reply.snapshot_list()); + } + + return output; +} + std::string mp::CSVFormatter::format(const NetworksReply& reply) const { fmt::memory_buffer buf; diff --git a/src/client/cli/formatter/json_formatter.cpp b/src/client/cli/formatter/json_formatter.cpp index 4ddd2f44699..3377eea39ca 100644 --- a/src/client/cli/formatter/json_formatter.cpp +++ b/src/client/cli/formatter/json_formatter.cpp @@ -22,7 +22,6 @@ #include #include -#include #include namespace mp = multipass; @@ -55,108 +54,133 @@ QJsonObject format_images(const google::protobuf::RepeatedPtrField #include +#include + namespace mp = multipass; namespace { +const std::regex newline("(\r\n|\n)"); + std::string format_images(const google::protobuf::RepeatedPtrField& images_info, std::string type) { @@ -53,119 +57,204 @@ std::string to_usage(const std::string& usage, const std::string& total) return fmt::format("{} out of {}", mp::MemorySize{usage}.human_readable(), mp::MemorySize{total}.human_readable()); } -} // namespace -std::string mp::TableFormatter::format(const InfoReply& reply) const +std::string generate_snapshot_details(const mp::DetailedInfoItem& item) { fmt::memory_buffer buf; + const auto& fundamentals = item.snapshot_info().fundamentals(); + + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Snapshot:", fundamentals.snapshot_name()); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Instance:", item.name()); + + if (!item.snapshot_info().size().empty()) + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Size:", item.snapshot_info().size()); - for (const auto& info : format::sorted(reply.info())) + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "CPU(s):", item.cpu_count()); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Disk space:", item.disk_total()); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Memory size:", item.memory_total()); + + auto mount_paths = item.mount_info().mount_paths(); + fmt::format_to(std::back_inserter(buf), "{:<16}{}", "Mounts:", mount_paths.empty() ? "--\n" : ""); + for (auto mount = mount_paths.cbegin(); mount != mount_paths.cend(); ++mount) { - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Name:", info.name()); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", - "State:", mp::format::status_string_for(info.instance_status())); + if (mount != mount_paths.cbegin()) + fmt::format_to(std::back_inserter(buf), "{:<16}", ""); + fmt::format_to(std::back_inserter(buf), + "{:{}} => {}\n", + mount->source_path(), + item.mount_info().longest_path_len(), + mount->target_path()); + } - int ipv4_size = info.ipv4_size(); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "IPv4:", ipv4_size ? info.ipv4(0) : "--"); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Created:", + google::protobuf::util::TimeUtil::ToString(fundamentals.creation_timestamp())); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Parent:", + fundamentals.parent().empty() ? "--" : fundamentals.parent()); + + auto children = item.snapshot_info().children(); + fmt::format_to(std::back_inserter(buf), "{:<16}{}", "Children:", children.empty() ? "--\n" : ""); + for (auto child = children.cbegin(); child != children.cend(); ++child) + { + if (child != children.cbegin()) + fmt::format_to(std::back_inserter(buf), "{:<16}", ""); + fmt::format_to(std::back_inserter(buf), "{}\n", *child); + } - for (int i = 1; i < ipv4_size; ++i) - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "", info.ipv4(i)); + /* TODO split and align string if it extends onto several lines; but actually better implement generic word-wrapping + for all output, taking both terminal width and current indentation level into account */ + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Comment:", + fundamentals.comment().empty() + ? "--" + : std::regex_replace(fundamentals.comment(), newline, "$&" + std::string(16, ' '))); - if (int ipv6_size = info.ipv6_size()) - { - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "IPv6:", info.ipv6(0)); + return fmt::to_string(buf); +} - for (int i = 1; i < ipv6_size; ++i) - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "", info.ipv6(i)); - } +std::string generate_instance_details(const mp::DetailedInfoItem& item) +{ + fmt::memory_buffer buf; + const auto& instance_details = item.instance_info(); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", - "Release:", info.current_release().empty() ? "--" : info.current_release()); - fmt::format_to(std::back_inserter(buf), "{:<16}", "Image hash:"); - if (info.id().empty()) - fmt::format_to(std::back_inserter(buf), "{}\n", "Not Available"); - else - fmt::format_to(std::back_inserter(buf), "{}{}\n", info.id().substr(0, 12), - !info.image_release().empty() ? fmt::format(" (Ubuntu {})", info.image_release()) : ""); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Name:", item.name()); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "State:", + mp::format::status_string_for(item.instance_status())); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Snapshots:", instance_details.num_snapshots()); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", - "CPU(s):", info.cpu_count().empty() ? "--" : info.cpu_count()); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Load:", info.load().empty() ? "--" : info.load()); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", - "Disk usage:", to_usage(info.disk_usage(), info.disk_total())); - fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", - "Memory usage:", to_usage(info.memory_usage(), info.memory_total())); + int ipv4_size = instance_details.ipv4_size(); + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "IPv4:", ipv4_size ? instance_details.ipv4(0) : "--"); - auto mount_paths = info.mount_info().mount_paths(); - fmt::format_to(std::back_inserter(buf), "{:<16}{}", "Mounts:", mount_paths.empty() ? "--\n" : ""); + for (int i = 1; i < ipv4_size; ++i) + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "", instance_details.ipv4(i)); - for (auto mount = mount_paths.cbegin(); mount != mount_paths.cend(); ++mount) - { - if (mount != mount_paths.cbegin()) - fmt::format_to(std::back_inserter(buf), "{:<16}", ""); - fmt::format_to(std::back_inserter(buf), "{:{}} => {}\n", mount->source_path(), - info.mount_info().longest_path_len(), mount->target_path()); - - auto mount_maps = mount->mount_maps(); - auto uid_mappings_size = mount_maps.uid_mappings_size(); - - for (auto i = 0; i < uid_mappings_size; ++i) - { - auto uid_map_pair = mount_maps.uid_mappings(i); - auto host_uid = uid_map_pair.host_id(); - auto instance_uid = uid_map_pair.instance_id(); - - fmt::format_to(std::back_inserter(buf), "{:>{}}{}:{}{}", (i == 0) ? "UID map: " : "", (i == 0) ? 29 : 0, - std::to_string(host_uid), - (instance_uid == mp::default_id) ? "default" : std::to_string(instance_uid), - (i == uid_mappings_size - 1) ? "\n" : ", "); - } - - for (auto gid_mapping = mount_maps.gid_mappings().cbegin(); gid_mapping != mount_maps.gid_mappings().cend(); - ++gid_mapping) - { - auto host_gid = gid_mapping->host_id(); - auto instance_gid = gid_mapping->instance_id(); - - fmt::format_to(std::back_inserter(buf), "{:>{}}{}:{}{}{}", - (gid_mapping == mount_maps.gid_mappings().cbegin()) ? "GID map: " : "", - (gid_mapping == mount_maps.gid_mappings().cbegin()) ? 29 : 0, std::to_string(host_gid), - (instance_gid == mp::default_id) ? "default" : std::to_string(instance_gid), - (std::next(gid_mapping) != mount_maps.gid_mappings().cend()) ? ", " : "", - (std::next(gid_mapping) == mount_maps.gid_mappings().cend()) ? "\n" : ""); - } - } + if (int ipv6_size = instance_details.ipv6_size()) + { + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "IPv6:", instance_details.ipv6(0)); - fmt::format_to(std::back_inserter(buf), "\n"); + for (int i = 1; i < ipv6_size; ++i) + fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "", instance_details.ipv6(i)); } - auto output = fmt::to_string(buf); - if (!reply.info().empty()) - output.pop_back(); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Release:", + instance_details.current_release().empty() ? "--" : instance_details.current_release()); + fmt::format_to(std::back_inserter(buf), "{:<16}", "Image hash:"); + if (instance_details.id().empty()) + fmt::format_to(std::back_inserter(buf), "{}\n", "Not Available"); else - output = "\n"; + fmt::format_to(std::back_inserter(buf), + "{}{}\n", + instance_details.id().substr(0, 12), + !instance_details.image_release().empty() + ? fmt::format(" (Ubuntu {})", instance_details.image_release()) + : ""); + + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "CPU(s):", + item.cpu_count().empty() ? "--" : item.cpu_count()); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Load:", + instance_details.load().empty() ? "--" : instance_details.load()); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Disk usage:", + to_usage(instance_details.disk_usage(), item.disk_total())); + fmt::format_to(std::back_inserter(buf), + "{:<16}{}\n", + "Memory usage:", + to_usage(instance_details.memory_usage(), item.memory_total())); + + const auto& mount_paths = item.mount_info().mount_paths(); + fmt::format_to(std::back_inserter(buf), "{:<16}{}", "Mounts:", mount_paths.empty() ? "--\n" : ""); + + for (auto mount = mount_paths.cbegin(); mount != mount_paths.cend(); ++mount) + { + if (mount != mount_paths.cbegin()) + fmt::format_to(std::back_inserter(buf), "{:<16}", ""); + fmt::format_to(std::back_inserter(buf), + "{:{}} => {}\n", + mount->source_path(), + item.mount_info().longest_path_len(), + mount->target_path()); + + auto mount_maps = mount->mount_maps(); + auto uid_mappings_size = mount_maps.uid_mappings_size(); + + for (auto i = 0; i < uid_mappings_size; ++i) + { + auto uid_map_pair = mount_maps.uid_mappings(i); + auto host_uid = uid_map_pair.host_id(); + auto instance_uid = uid_map_pair.instance_id(); + + fmt::format_to(std::back_inserter(buf), + "{:>{}}{}:{}{}", + (i == 0) ? "UID map: " : "", + (i == 0) ? 29 : 0, + std::to_string(host_uid), + (instance_uid == mp::default_id) ? "default" : std::to_string(instance_uid), + (i == uid_mappings_size - 1) ? "\n" : ", "); + } - return output; + for (auto gid_mapping = mount_maps.gid_mappings().cbegin(); gid_mapping != mount_maps.gid_mappings().cend(); + ++gid_mapping) + { + auto host_gid = gid_mapping->host_id(); + auto instance_gid = gid_mapping->instance_id(); + + fmt::format_to(std::back_inserter(buf), + "{:>{}}{}:{}{}{}", + (gid_mapping == mount_maps.gid_mappings().cbegin()) ? "GID map: " : "", + (gid_mapping == mount_maps.gid_mappings().cbegin()) ? 29 : 0, + std::to_string(host_gid), + (instance_gid == mp::default_id) ? "default" : std::to_string(instance_gid), + (std::next(gid_mapping) != mount_maps.gid_mappings().cend()) ? ", " : "", + (std::next(gid_mapping) == mount_maps.gid_mappings().cend()) ? "\n" : ""); + } + } + + return fmt::to_string(buf); } -std::string mp::TableFormatter::format(const ListReply& reply) const +std::string generate_instances_list(const mp::InstancesList& instance_list) { fmt::memory_buffer buf; - auto instances = reply.instances(); + const auto& instances = instance_list.instances(); if (instances.empty()) return "No instances found.\n"; + const std::string name_col_header = "Name"; const auto name_column_width = mp::format::column_width( - instances.begin(), instances.end(), [](const auto& instance) -> int { return instance.name().length(); }, 24); + instances.begin(), + instances.end(), + [](const auto& instance) -> int { return instance.name().length(); }, + name_col_header.length(), + 24); const std::string::size_type state_column_width = 18; const std::string::size_type ip_column_width = 17; const auto row_format = "{:<{}}{:<{}}{:<{}}{:<}\n"; - fmt::format_to(std::back_inserter(buf), row_format, "Name", name_column_width, "State", state_column_width, "IPv4", - ip_column_width, "Image"); - - for (const auto& instance : format::sorted(reply.instances())) + fmt::format_to(std::back_inserter(buf), + row_format, + name_col_header, + name_column_width, + "State", + state_column_width, + "IPv4", + ip_column_width, + "Image"); + + for (const auto& instance : mp::format::sorted(instance_list.instances())) { int ipv4_size = instance.ipv4_size(); @@ -185,6 +274,116 @@ std::string mp::TableFormatter::format(const ListReply& reply) const return fmt::to_string(buf); } +std::string generate_snapshots_list(const mp::SnapshotsList& snapshot_list) +{ + fmt::memory_buffer buf; + + const auto& snapshots = snapshot_list.snapshots(); + + if (snapshots.empty()) + return "No snapshots found.\n"; + + const std::string name_col_header = "Instance", snapshot_col_header = "Snapshot", parent_col_header = "Parent", + comment_col_header = "Comment"; + const auto name_column_width = mp::format::column_width( + snapshots.begin(), + snapshots.end(), + [](const auto& snapshot) -> int { return snapshot.name().length(); }, + name_col_header.length()); + const auto snapshot_column_width = mp::format::column_width( + snapshots.begin(), + snapshots.end(), + [](const auto& snapshot) -> int { return snapshot.fundamentals().snapshot_name().length(); }, + snapshot_col_header.length()); + const auto parent_column_width = mp::format::column_width( + snapshots.begin(), + snapshots.end(), + [](const auto& snapshot) -> int { return snapshot.fundamentals().parent().length(); }, + parent_col_header.length()); + + const auto row_format = "{:<{}}{:<{}}{:<{}}{:<}\n"; + fmt::format_to(std::back_inserter(buf), + row_format, + name_col_header, + name_column_width, + snapshot_col_header, + snapshot_column_width, + parent_col_header, + parent_column_width, + comment_col_header); + + for (const auto& snapshot : mp::format::sorted(snapshot_list.snapshots())) + { + size_t max_comment_column_width = 50; + std::smatch match; + const auto& fundamentals = snapshot.fundamentals(); + + if (std::regex_search(fundamentals.comment().begin(), fundamentals.comment().end(), match, newline)) + max_comment_column_width = std::min((size_t)(match.position(1)) + 1, max_comment_column_width); + + fmt::format_to(std::back_inserter(buf), + row_format, + snapshot.name(), + name_column_width, + fundamentals.snapshot_name(), + snapshot_column_width, + fundamentals.parent().empty() ? "--" : fundamentals.parent(), + parent_column_width, + fundamentals.comment().empty() ? "--" + : fundamentals.comment().length() > max_comment_column_width + ? fmt::format("{}…", fundamentals.comment().substr(0, max_comment_column_width - 1)) + : fundamentals.comment()); + } + + return fmt::to_string(buf); +} +} // namespace + +std::string mp::TableFormatter::format(const InfoReply& reply) const +{ + fmt::memory_buffer buf; + + for (const auto& info : mp::format::sorted(reply.details())) + { + if (info.has_instance_info()) + { + fmt::format_to(std::back_inserter(buf), generate_instance_details(info)); + } + else + { + assert(info.has_snapshot_info() && "either one of instance or snapshot details should be populated"); + fmt::format_to(std::back_inserter(buf), generate_snapshot_details(info)); + } + + fmt::format_to(std::back_inserter(buf), "\n"); + } + + std::string output = fmt::to_string(buf); + if (!reply.details().empty()) + output.pop_back(); + else + output = fmt::format("No {} found.\n", reply.snapshots() ? "snapshots" : "instances"); + + return output; +} + +std::string mp::TableFormatter::format(const ListReply& reply) const +{ + std::string output; + + if (reply.has_instance_list()) + { + output = generate_instances_list(reply.instance_list()); + } + else + { + assert(reply.has_snapshot_list() && "either one of instances or snapshots should be populated"); + output = generate_snapshots_list(reply.snapshot_list()); + } + + return output; +} + std::string mp::TableFormatter::format(const NetworksReply& reply) const { fmt::memory_buffer buf; @@ -194,17 +393,26 @@ std::string mp::TableFormatter::format(const NetworksReply& reply) const if (interfaces.empty()) return "No network interfaces found.\n"; + const std::string name_col_header = "Name", type_col_header = "Type", desc_col_header = "Description"; const auto name_column_width = mp::format::column_width( - interfaces.begin(), interfaces.end(), [](const auto& interface) -> int { return interface.name().length(); }, - 5); - + interfaces.begin(), + interfaces.end(), + [](const auto& interface) -> int { return interface.name().length(); }, + name_col_header.length()); const auto type_column_width = mp::format::column_width( - interfaces.begin(), interfaces.end(), [](const auto& interface) -> int { return interface.type().length(); }, - 5); + interfaces.begin(), + interfaces.end(), + [](const auto& interface) -> int { return interface.type().length(); }, + type_col_header.length()); const auto row_format = "{:<{}}{:<{}}{:<}\n"; - fmt::format_to(std::back_inserter(buf), row_format, "Name", name_column_width, "Type", type_column_width, - "Description"); + fmt::format_to(std::back_inserter(buf), + row_format, + name_col_header, + name_column_width, + type_col_header, + type_column_width, + desc_col_header); for (const auto& interface : format::sorted(reply.interfaces())) { @@ -276,31 +484,55 @@ std::string mp::TableFormatter::format(const mp::AliasDict& aliases) const if (aliases.empty()) return "No aliases defined.\n"; - auto width = [&aliases](const auto get_width, int minimum_width) -> int { + auto width = [&aliases](const auto get_width, int header_width) -> int { return mp::format::column_width( - aliases.cbegin(), aliases.cend(), - [&, get_width, minimum_width](const auto& ctx) -> int { - return mp::format::column_width( - ctx.second.cbegin(), ctx.second.cend(), - [&get_width](const auto& alias) -> int { return get_width(alias); }, minimum_width, 2); + aliases.cbegin(), + aliases.cend(), + [&, get_width](const auto& ctx) -> int { + return get_width(*std::max_element( + ctx.second.cbegin(), + ctx.second.cend(), + [&get_width](const auto& lhs, const auto& rhs) { return get_width(lhs) < get_width(rhs); })); }, - minimum_width, 0); + header_width); }; - const auto alias_width = width([](const auto& alias) -> int { return alias.first.length(); }, 7); - const auto instance_width = width([](const auto& alias) -> int { return alias.second.instance.length(); }, 10); - const auto command_width = width([](const auto& alias) -> int { return alias.second.command.length(); }, 9); + const std::string alias_col_header = "Alias", instance_col_header = "Instance", command_col_header = "Command", + context_col_header = "Context", dir_col_header = "Working directory"; + const std::string active_context = "*"; + const auto alias_width = + width([](const auto& alias) -> int { return alias.first.length(); }, alias_col_header.length()); + const auto instance_width = + width([](const auto& alias) -> int { return alias.second.instance.length(); }, instance_col_header.length()); + const auto command_width = + width([](const auto& alias) -> int { return alias.second.command.length(); }, command_col_header.length()); const auto context_width = mp::format::column_width( - aliases.cbegin(), aliases.cend(), [](const auto& alias) -> int { return alias.first.length(); }, 10); + aliases.cbegin(), + aliases.cend(), + [&aliases, &active_context](const auto& alias) -> int { + return alias.first == aliases.active_context_name() ? alias.first.length() + active_context.length() + : alias.first.length(); + }, + context_col_header.length()); const auto row_format = "{:<{}}{:<{}}{:<{}}{:<{}}{:<}\n"; - fmt::format_to(std::back_inserter(buf), row_format, "Alias", alias_width, "Instance", instance_width, "Command", - command_width, "Context", context_width, "Working directory"); + fmt::format_to(std::back_inserter(buf), + row_format, + alias_col_header, + alias_width, + instance_col_header, + instance_width, + command_col_header, + command_width, + context_col_header, + context_width, + dir_col_header); for (const auto& [context_name, context_contents] : sort_dict(aliases)) { - std::string shown_context = context_name == aliases.active_context_name() ? context_name + "*" : context_name; + std::string shown_context = + context_name == aliases.active_context_name() ? context_name + active_context : context_name; for (const auto& [name, def] : sort_dict(context_contents)) { diff --git a/src/client/cli/formatter/yaml_formatter.cpp b/src/client/cli/formatter/yaml_formatter.cpp index 4b92b41160d..7f263f0230f 100644 --- a/src/client/cli/formatter/yaml_formatter.cpp +++ b/src/client/cli/formatter/yaml_formatter.cpp @@ -31,8 +31,8 @@ namespace mpu = multipass::utils; namespace { -std::map -format_images(const google::protobuf::RepeatedPtrField& images_info) +template +std::map format_images(const ImageInfo& images_info) { std::map images_node; @@ -57,107 +57,121 @@ format_images(const google::protobuf::RepeatedPtrField& return images_node; } -} // namespace -std::string mp::YamlFormatter::format(const InfoReply& reply) const +YAML::Node generate_snapshot_details(const mp::DetailedInfoItem& item) { - YAML::Node info_node; + const auto& snapshot_details = item.snapshot_info(); + const auto& fundamentals = snapshot_details.fundamentals(); + YAML::Node snapshot_node; - info_node["errors"].push_back(YAML::Null); + snapshot_node["size"] = snapshot_details.size().empty() ? YAML::Node() : YAML::Node(snapshot_details.size()); + snapshot_node["cpu_count"] = item.cpu_count(); + snapshot_node["disk_space"] = item.disk_total(); + snapshot_node["memory_size"] = item.memory_total(); - for (const auto& info : format::sorted(reply.info())) + YAML::Node mounts; + for (const auto& mount : item.mount_info().mount_paths()) { - YAML::Node instance_node; + YAML::Node mount_node; + mount_node["source_path"] = mount.source_path(); + mounts[mount.target_path()] = mount_node; + } + snapshot_node["mounts"] = mounts; - instance_node["state"] = mp::format::status_string_for(info.instance_status()); - instance_node["image_hash"] = info.id(); - instance_node["image_release"] = info.image_release(); - if (info.current_release().empty()) - instance_node["release"] = YAML::Null; - else - instance_node["release"] = info.current_release(); + snapshot_node["created"] = google::protobuf::util::TimeUtil::ToString(fundamentals.creation_timestamp()); + snapshot_node["parent"] = fundamentals.parent().empty() ? YAML::Node() : YAML::Node(fundamentals.parent()); - instance_node["cpu_count"] = YAML::Null; - if (!info.cpu_count().empty()) - instance_node["cpu_count"] = info.cpu_count(); + snapshot_node["children"] = YAML::Node(YAML::NodeType::Sequence); + for (const auto& child : snapshot_details.children()) + snapshot_node["children"].push_back(child); - if (!info.load().empty()) - { - // The VM returns load info in the default C locale - auto current_loc = std::locale(); - std::locale::global(std::locale("C")); - auto loads = mp::utils::split(info.load(), " "); - for (const auto& entry : loads) - instance_node["load"].push_back(entry); - std::locale::global(current_loc); - } + snapshot_node["comment"] = fundamentals.comment().empty() ? YAML::Node() : YAML::Node(fundamentals.comment()); - YAML::Node disk; - disk["used"] = YAML::Null; - disk["total"] = YAML::Null; - if (!info.disk_usage().empty()) - disk["used"] = info.disk_usage(); - if (!info.disk_total().empty()) - disk["total"] = info.disk_total(); - - // TODO: disk name should come from daemon - YAML::Node disk_node; - disk_node["sda1"] = disk; - instance_node["disks"].push_back(disk_node); - - YAML::Node memory; - memory["usage"] = YAML::Null; - memory["total"] = YAML::Null; - if (!info.memory_usage().empty()) - memory["usage"] = std::stoll(info.memory_usage()); - if (!info.memory_total().empty()) - memory["total"] = std::stoll(info.memory_total()); - - instance_node["memory"] = memory; + return snapshot_node; +} - instance_node["ipv4"] = YAML::Node(YAML::NodeType::Sequence); - for (const auto& ip : info.ipv4()) - instance_node["ipv4"].push_back(ip); +YAML::Node generate_instance_details(const mp::DetailedInfoItem& item) +{ + const auto& instance_details = item.instance_info(); + YAML::Node instance_node; + + instance_node["state"] = mp::format::status_string_for(item.instance_status()); + instance_node["snapshot_count"] = instance_details.num_snapshots(); + instance_node["image_hash"] = instance_details.id(); + instance_node["image_release"] = instance_details.image_release(); + instance_node["release"] = + instance_details.current_release().empty() ? YAML::Node() : YAML::Node(instance_details.current_release()); + instance_node["cpu_count"] = item.cpu_count().empty() ? YAML::Node() : YAML::Node(item.cpu_count()); + + if (!instance_details.load().empty()) + { + // The VM returns load info in the default C locale + auto current_loc = std::locale(); + std::locale::global(std::locale("C")); + auto loads = mp::utils::split(instance_details.load(), " "); + for (const auto& entry : loads) + instance_node["load"].push_back(entry); + std::locale::global(current_loc); + } + + YAML::Node disk; + disk["used"] = instance_details.disk_usage().empty() ? YAML::Node() : YAML::Node(instance_details.disk_usage()); + disk["total"] = item.disk_total().empty() ? YAML::Node() : YAML::Node(item.disk_total()); + + // TODO: disk name should come from daemon + YAML::Node disk_node; + disk_node["sda1"] = disk; + instance_node["disks"].push_back(disk_node); + + YAML::Node memory; + memory["usage"] = instance_details.memory_usage().empty() ? YAML::Node() + : YAML::Node(std::stoll(instance_details.memory_usage())); + memory["total"] = item.memory_total().empty() ? YAML::Node() : YAML::Node(std::stoll(item.memory_total())); + instance_node["memory"] = memory; - YAML::Node mounts; - for (const auto& mount : info.mount_info().mount_paths()) + instance_node["ipv4"] = YAML::Node(YAML::NodeType::Sequence); + for (const auto& ip : instance_details.ipv4()) + instance_node["ipv4"].push_back(ip); + + YAML::Node mounts; + for (const auto& mount : item.mount_info().mount_paths()) + { + YAML::Node mount_node; + + for (const auto& uid_mapping : mount.mount_maps().uid_mappings()) { - YAML::Node mount_node; - - for (const auto& uid_mapping : mount.mount_maps().uid_mappings()) - { - auto host_uid = uid_mapping.host_id(); - auto instance_uid = uid_mapping.instance_id(); - - mount_node["uid_mappings"].push_back( - fmt::format("{}:{}", std::to_string(host_uid), - (instance_uid == mp::default_id) ? "default" : std::to_string(instance_uid))); - } - for (const auto& gid_mapping : mount.mount_maps().gid_mappings()) - { - auto host_gid = gid_mapping.host_id(); - auto instance_gid = gid_mapping.instance_id(); - - mount_node["gid_mappings"].push_back( - fmt::format("{}:{}", std::to_string(host_gid), - (instance_gid == mp::default_id) ? "default" : std::to_string(instance_gid))); - } - - mount_node["source_path"] = mount.source_path(); - mounts[mount.target_path()] = mount_node; + auto host_uid = uid_mapping.host_id(); + auto instance_uid = uid_mapping.instance_id(); + + mount_node["uid_mappings"].push_back( + fmt::format("{}:{}", + std::to_string(host_uid), + (instance_uid == mp::default_id) ? "default" : std::to_string(instance_uid))); } - instance_node["mounts"] = mounts; + for (const auto& gid_mapping : mount.mount_maps().gid_mappings()) + { + auto host_gid = gid_mapping.host_id(); + auto instance_gid = gid_mapping.instance_id(); - info_node[info.name()].push_back(instance_node); + mount_node["gid_mappings"].push_back( + fmt::format("{}:{}", + std::to_string(host_gid), + (instance_gid == mp::default_id) ? "default" : std::to_string(instance_gid))); + } + + mount_node["source_path"] = mount.source_path(); + mounts[mount.target_path()] = mount_node; } - return mpu::emit_yaml(info_node); + instance_node["mounts"] = mounts; + + return instance_node; } -std::string mp::YamlFormatter::format(const ListReply& reply) const +std::string generate_instances_list(const mp::InstancesList& instance_list) { YAML::Node list; - for (const auto& instance : format::sorted(reply.instances())) + for (const auto& instance : mp::format::sorted(instance_list.instances())) { YAML::Node instance_node; instance_node["state"] = mp::format::status_string_for(instance.instance_status()); @@ -175,6 +189,70 @@ std::string mp::YamlFormatter::format(const ListReply& reply) const return mpu::emit_yaml(list); } +std::string generate_snapshots_list(const mp::SnapshotsList& snapshot_list) +{ + YAML::Node info_node; + + for (const auto& item : mp::format::sorted(snapshot_list.snapshots())) + { + const auto& snapshot = item.fundamentals(); + YAML::Node instance_node; + YAML::Node snapshot_node; + + snapshot_node["parent"] = snapshot.parent().empty() ? YAML::Node() : YAML::Node(snapshot.parent()); + snapshot_node["comment"] = snapshot.comment().empty() ? YAML::Node() : YAML::Node(snapshot.comment()); + + instance_node[snapshot.snapshot_name()].push_back(snapshot_node); + info_node[item.name()].push_back(instance_node); + } + + return mpu::emit_yaml(info_node); +} +} // namespace + +std::string mp::YamlFormatter::format(const InfoReply& reply) const +{ + YAML::Node info_node; + + info_node["errors"].push_back(YAML::Null); + + for (const auto& info : mp::format::sorted(reply.details())) + { + if (info.has_instance_info()) + { + info_node[info.name()].push_back(generate_instance_details(info)); + } + else + { + assert(info.has_snapshot_info() && "either one of instance or snapshot details should be populated"); + + YAML::Node snapshot_node; + snapshot_node[info.snapshot_info().fundamentals().snapshot_name()] = generate_snapshot_details(info); + + info_node[info.name()][0]["snapshots"].push_back(snapshot_node); + } + } + + return mpu::emit_yaml(info_node); +} + +std::string mp::YamlFormatter::format(const ListReply& reply) const +{ + std::string output; + + if (reply.has_instance_list()) + { + output = generate_instances_list(reply.instance_list()); + } + else + { + assert(reply.has_snapshot_list() && "eitherr one of instances or snapshots should be populated"); + output = generate_snapshots_list(reply.snapshot_list()); + } + + return output; +} + std::string mp::YamlFormatter::format(const NetworksReply& reply) const { YAML::Node list; diff --git a/src/client/common/alias_dict.cpp b/src/client/common/alias_dict.cpp index 48fce686fc6..38a21af4ab6 100644 --- a/src/client/common/alias_dict.cpp +++ b/src/client/common/alias_dict.cpp @@ -394,37 +394,7 @@ void mp::AliasDict::load_dict() void mp::AliasDict::save_dict() { sanitize_contexts(); - - QJsonObject dict_json = to_json(); - - auto config_file_name = QString::fromStdString(aliases_file); - - QTemporaryFile temp_file{mpu::create_temp_file_with_path(config_file_name)}; - - if (MP_FILEOPS.open(temp_file, QIODevice::ReadWrite)) - { - temp_file.setAutoRemove(false); - - mp::write_json(dict_json, temp_file.fileName()); - - temp_file.close(); - - if (MP_FILEOPS.exists(QFile{config_file_name})) - { - auto backup_file_name = config_file_name + ".bak"; - QFile backup_file(backup_file_name); - - if (MP_FILEOPS.exists(backup_file) && !MP_FILEOPS.remove(backup_file)) - throw std::runtime_error(fmt::format("cannot remove old aliases backup file {}", backup_file_name)); - - QFile config_file(config_file_name); - if (!MP_FILEOPS.rename(config_file, backup_file_name)) - throw std::runtime_error(fmt::format("cannot rename aliases config to {}", backup_file_name)); - } - - if (!MP_FILEOPS.rename(temp_file, config_file_name)) - throw std::runtime_error(fmt::format("cannot create aliases config file {}", config_file_name)); - } + MP_JSONUTILS.write_json(to_json(), QString::fromStdString(aliases_file)); } // This function removes the contexts which do not contain aliases, except the active context. diff --git a/src/client/gui/gui_cmd.cpp b/src/client/gui/gui_cmd.cpp index 655e6cfb285..0d36c978b58 100644 --- a/src/client/gui/gui_cmd.cpp +++ b/src/client/gui/gui_cmd.cpp @@ -191,14 +191,15 @@ void cmd::GuiCmd::update_menu() auto reply = list_future.result(); - handle_petenv_instance(reply.instances()); + handle_petenv_instance(reply.instance_list().instances()); for (auto it = instances_entries.cbegin(); it != instances_entries.cend(); ++it) { - auto instance = std::find_if(reply.instances().cbegin(), reply.instances().cend(), + auto instance = std::find_if(reply.instance_list().instances().cbegin(), + reply.instance_list().instances().cend(), [it](const ListVMInstance& instance) { return it->first == instance.name(); }); - if (instance == reply.instances().cend()) + if (instance == reply.instance_list().instances().cend()) { instances_to_remove.push_back(it->first); } @@ -209,7 +210,7 @@ void cmd::GuiCmd::update_menu() instances_entries.erase(instance); } - for (const auto& instance : reply.instances()) + for (const auto& instance : reply.instance_list().instances()) { auto name = instance.name(); auto state = instance.instance_status(); diff --git a/src/daemon/CMakeLists.txt b/src/daemon/CMakeLists.txt index 7b692da0137..cd555926205 100644 --- a/src/daemon/CMakeLists.txt +++ b/src/daemon/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(daemon STATIC daemon_rpc.cpp default_vm_image_vault.cpp instance_settings_handler.cpp + snapshot_settings_handler.cpp ubuntu_image_host.cpp) include_directories(daemon diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index a7d287072dc..a3c1be70626 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -18,6 +18,7 @@ #include "daemon.h" #include "base_cloud_init_config.h" #include "instance_settings_handler.h" +#include "snapshot_settings_handler.h" #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -322,30 +325,8 @@ std::unordered_map load_db(const mp::Path& data_path, for (QJsonValueRef entry : record["mounts"].toArray()) { - mp::id_mappings uid_mappings; - mp::id_mappings gid_mappings; - - auto target_path = entry.toObject()["target_path"].toString().toStdString(); - auto source_path = entry.toObject()["source_path"].toString().toStdString(); - - for (QJsonValueRef uid_entry : entry.toObject()["uid_mappings"].toArray()) - { - uid_mappings.push_back( - {uid_entry.toObject()["host_uid"].toInt(), uid_entry.toObject()["instance_uid"].toInt()}); - } - - for (QJsonValueRef gid_entry : entry.toObject()["gid_mappings"].toArray()) - { - gid_mappings.push_back( - {gid_entry.toObject()["host_gid"].toInt(), gid_entry.toObject()["instance_gid"].toInt()}); - } - - uid_mappings = mp::unique_id_mappings(uid_mappings); - gid_mappings = mp::unique_id_mappings(gid_mappings); - auto mount_type = mp::VMMount::MountType(entry.toObject()["mount_type"].toInt()); - - mp::VMMount mount{source_path, gid_mappings, uid_mappings, mount_type}; - mounts[target_path] = mount; + const auto& json = entry.toObject(); + mounts[json["target_path"].toString().toStdString()] = mp::VMMount{json}; } reconstructed_records[key] = {num_cores, @@ -397,37 +378,8 @@ QJsonObject vm_spec_to_json(const mp::VMSpecs& specs) QJsonArray json_mounts; for (const auto& mount : specs.mounts) { - QJsonObject entry; - entry.insert("source_path", QString::fromStdString(mount.second.source_path)); + auto entry = mount.second.serialize(); entry.insert("target_path", QString::fromStdString(mount.first)); - - QJsonArray uid_mappings; - - for (const auto& map : mount.second.uid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_uid", map.first); - map_entry.insert("instance_uid", map.second); - - uid_mappings.append(map_entry); - } - - entry.insert("uid_mappings", uid_mappings); - - QJsonArray gid_mappings; - - for (const auto& map : mount.second.gid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_gid", map.first); - map_entry.insert("instance_gid", map.second); - - gid_mappings.append(map_entry); - } - - entry.insert("gid_mappings", gid_mappings); - - entry.insert("mount_type", static_cast(mount.second.mount_type)); json_mounts.append(entry); } @@ -435,14 +387,20 @@ QJsonObject vm_spec_to_json(const mp::VMSpecs& specs) return json; } -auto fetch_image_for(const std::string& name, const mp::FetchType& fetch_type, mp::VMImageVault& vault) +auto fetch_image_for(const std::string& name, mp::VirtualMachineFactory& factory, mp::VMImageVault& vault) { auto stub_prepare = [](const mp::VMImage&) -> mp::VMImage { return {}; }; auto stub_progress = [](int download_type, int progress) { return true; }; mp::Query query{name, "", false, "", mp::Query::Type::Alias, false}; - return vault.fetch_image(fetch_type, query, stub_prepare, stub_progress, false, std::nullopt); + return vault.fetch_image(factory.fetch_type(), + query, + stub_prepare, + stub_progress, + false, + std::nullopt, + factory.get_instance_directory(name)); } auto try_mem_size(const std::string& val) -> std::optional @@ -655,6 +613,8 @@ auto connect_rpc(mp::DaemonRpc& rpc, mp::Daemon& daemon) QObject::connect(&rpc, &mp::DaemonRpc::on_set, &daemon, &mp::Daemon::set); QObject::connect(&rpc, &mp::DaemonRpc::on_keys, &daemon, &mp::Daemon::keys); QObject::connect(&rpc, &mp::DaemonRpc::on_authenticate, &daemon, &mp::Daemon::authenticate); + QObject::connect(&rpc, &mp::DaemonRpc::on_snapshot, &daemon, &mp::Daemon::snapshot); + QObject::connect(&rpc, &mp::DaemonRpc::on_restore, &daemon, &mp::Daemon::restore); } enum class InstanceGroup @@ -665,7 +625,10 @@ enum class InstanceGroup All }; -using InstanceTable = std::unordered_map; +// Hack to import typedef here, without making it part of the Daemon's public interface +// clang-format off +struct TapDaemon : private mp::Daemon { using Daemon::InstanceTable; }; // clang-format on +using InstanceTable = TapDaemon::InstanceTable; using InstanceTrail = std::variant>; // missing instances @@ -738,10 +701,17 @@ InstanceSelectionReport select_instances(InstanceTable& operative_instances, Ins for (const auto& name : names) { - if (seen_instances.insert(name).second) + using T = std::decay_t; + const std::string* vm_name; + if constexpr (std::is_same_v) + vm_name = &name; + else + vm_name = &name.instance_name(); + + if (seen_instances.insert(*vm_name).second) { - auto trail = find_instance(operative_instances, deleted_instances, name); - rank_instance(name, trail, ret); + auto trail = find_instance(operative_instances, deleted_instances, *vm_name); + rank_instance(*vm_name, trail, ret); } } } @@ -1132,6 +1102,42 @@ bool is_ipv4_valid(const std::string& ipv4) return true; } +struct SnapshotPick +{ + std::unordered_set pick; + bool all_or_none; +}; + +using InstanceSnapshotPairs = google::protobuf::RepeatedPtrField; +using InstanceSnapshotsMap = std::unordered_map; +InstanceSnapshotsMap map_snapshots_to_instances(const InstanceSnapshotPairs& instances_snapshots) +{ + InstanceSnapshotsMap instance_snapshots_map; + + for (const auto& it : instances_snapshots) + { + const auto& instance = it.instance_name(); + auto& snapshot_pick = instance_snapshots_map[instance]; + + if (!it.has_snapshot_name()) + snapshot_pick.all_or_none = true; + else + snapshot_pick.pick.insert(it.snapshot_name()); + } + + return instance_snapshots_map; +} + +void verify_snapshot_picks(const InstanceSelectionReport& report, + const std::unordered_map& snapshot_picks) +{ + for (const auto& selection : {report.deleted_selection, report.operative_selection}) + for (const auto& vm_it : selection) + if (auto pick_it = snapshot_picks.find(vm_it->first); pick_it != snapshot_picks.end()) + for (const auto& snapshot_name : pick_it->second.pick) + vm_it->second->get_snapshot(snapshot_name); // throws if it doesn't exist +} + void add_aliases(google::protobuf::RepeatedPtrField* container, const std::string& remote_name, const mp::VMImageInfo& info, const std::string& default_remote) { @@ -1165,15 +1171,128 @@ auto timeout_for(const int requested_timeout, const int blueprint_timeout) return mp::default_timeout; } -mp::SettingsHandler* -register_instance_mod(std::unordered_map& vm_instance_specs, - std::unordered_map& vm_instances, - const std::unordered_map& deleted_instances, - const std::unordered_set& preparing_instances, - std::function instance_persister) +mp::SettingsHandler* register_instance_mod(std::unordered_map& vm_instance_specs, + InstanceTable& operative_instances, + const InstanceTable& deleted_instances, + const std::unordered_set& preparing_instances, + std::function instance_persister) +{ + return MP_SETTINGS.register_handler(std::make_unique(vm_instance_specs, + operative_instances, + deleted_instances, + preparing_instances, + std::move(instance_persister))); +} + +mp::SettingsHandler* register_snapshot_mod( + std::unordered_map& operative_instances, + const std::unordered_map& deleted_instances, + const std::unordered_set& preparing_instances) { - return MP_SETTINGS.register_handler(std::make_unique( - vm_instance_specs, vm_instances, deleted_instances, preparing_instances, std::move(instance_persister))); + return MP_SETTINGS.register_handler( + std::make_unique(operative_instances, deleted_instances, preparing_instances)); +} + +// Erase any outdated mount handlers for a given VM +bool prune_obsolete_mounts(const std::unordered_map& mount_specs, + std::unordered_map& vm_mounts) +{ + auto removed = false; + auto handlers_it = vm_mounts.begin(); + while (handlers_it != vm_mounts.end()) + { + const auto& [target, handler] = *handlers_it; + if (auto specs_it = mount_specs.find(target); + specs_it == mount_specs.end() || handler->get_mount_spec() != specs_it->second) + { + if (handler->is_mount_managed_by_backend()) + { + assert(handler->is_active()); + handler->deactivate(); + } + + handlers_it = vm_mounts.erase(handlers_it); + removed = true; + } + else + ++handlers_it; + } + + return removed; +} + +void populate_snapshot_fundamentals(std::shared_ptr snapshot, + mp::SnapshotFundamentals* fundamentals) +{ + fundamentals->set_snapshot_name(snapshot->get_name()); + fundamentals->set_parent(snapshot->get_parents_name()); + fundamentals->set_comment(snapshot->get_comment()); + + auto timestamp = fundamentals->mutable_creation_timestamp(); + timestamp->set_seconds(snapshot->get_creation_timestamp().toSecsSinceEpoch()); + timestamp->set_nanos(snapshot->get_creation_timestamp().time().msec() * 1'000'000); +} + +void populate_mount_info(const std::unordered_map& mounts, + mp::MountInfo* mount_info, + bool& have_mounts) +{ + mount_info->set_longest_path_len(0); + + if (!mounts.empty()) + have_mounts = true; + + if (MP_SETTINGS.get_as(mp::mounts_key)) + { + for (const auto& mount : mounts) + { + if (mount.second.source_path.size() > mount_info->longest_path_len()) + mount_info->set_longest_path_len(mount.second.source_path.size()); + + auto entry = mount_info->add_mount_paths(); + entry->set_source_path(mount.second.source_path); + entry->set_target_path(mount.first); + + for (const auto& uid_mapping : mount.second.uid_mappings) + { + auto uid_pair = entry->mutable_mount_maps()->add_uid_mappings(); + uid_pair->set_host_id(uid_mapping.first); + uid_pair->set_instance_id(uid_mapping.second); + } + for (const auto& gid_mapping : mount.second.gid_mappings) + { + auto gid_pair = entry->mutable_mount_maps()->add_gid_mappings(); + gid_pair->set_host_id(gid_mapping.first); + gid_pair->set_instance_id(gid_mapping.second); + } + } + } +} + +void populate_snapshot_info(mp::VirtualMachine& vm, + std::shared_ptr snapshot, + mp::InfoReply& response, + bool& have_mounts) +{ + auto* info = response.add_details(); + auto snapshot_info = info->mutable_snapshot_info(); + auto fundamentals = snapshot_info->mutable_fundamentals(); + + info->set_name(vm.vm_name); + info->mutable_instance_status()->set_status(grpc_instance_status_for(snapshot->get_state())); + info->set_memory_total(snapshot->get_mem_size().human_readable()); + info->set_disk_total(snapshot->get_disk_space().human_readable()); + info->set_cpu_count(std::to_string(snapshot->get_num_cores())); + + auto mount_info = info->mutable_mount_info(); + populate_mount_info(snapshot->get_mounts(), mount_info, have_mounts); + + // TODO@snapshots get snapshot size once available + + for (const auto& child : vm.get_childrens_names(snapshot.get())) + snapshot_info->add_children(child); + + populate_snapshot_fundamentals(snapshot, fundamentals); } } // namespace @@ -1184,8 +1303,12 @@ mp::Daemon::Daemon(std::unique_ptr the_config) mp::utils::backend_directory_path(config->data_directory, config->factory->get_backend_directory_name()), mp::utils::backend_directory_path(config->cache_directory, config->factory->get_backend_directory_name()))}, daemon_rpc{config->server_address, *config->cert_provider, config->client_cert_store.get()}, - instance_mod_handler{register_instance_mod(vm_instance_specs, operative_instances, deleted_instances, - preparing_instances, [this] { persist_instances(); })} + instance_mod_handler{register_instance_mod(vm_instance_specs, + operative_instances, + deleted_instances, + preparing_instances, + [this] { persist_instances(); })}, + snapshot_mod_handler{register_snapshot_mod(operative_instances, deleted_instances, preparing_instances)} { connect_rpc(daemon_rpc, *this); std::vector invalid_specs; @@ -1223,7 +1346,7 @@ mp::Daemon::Daemon(std::unique_ptr the_config) continue; } - auto vm_image = fetch_image_for(name, config->factory->fetch_type(), *config->vault); + auto vm_image = fetch_image_for(name, *config->factory, *config->vault); if (!vm_image.image_path.isEmpty() && !QFile::exists(vm_image.image_path)) { mpl::log(mpl::Level::warning, category, @@ -1249,7 +1372,8 @@ mp::Daemon::Daemon(std::unique_ptr the_config) {}}; auto& instance_record = spec.deleted ? deleted_instances : operative_instances; - instance_record[name] = config->factory->create_virtual_machine(vm_desc, *this); + auto instance = instance_record[name] = config->factory->create_virtual_machine(vm_desc, *this); + instance->load_snapshots(); allocated_mac_addrs = std::move(new_macs); // Add the new macs to the daemon's list only if we got this far @@ -1338,7 +1462,10 @@ mp::Daemon::Daemon(std::unique_ptr the_config) mp::Daemon::~Daemon() { - mp::top_catch_all(category, [this] { MP_SETTINGS.unregister_handler(instance_mod_handler); }); + mp::top_catch_all(category, [this] { + MP_SETTINGS.unregister_handler(instance_mod_handler); + MP_SETTINGS.unregister_handler(snapshot_mod_handler); + }); } void mp::Daemon::create(const CreateRequest* request, @@ -1388,8 +1515,10 @@ try // clang-format on for (const auto& del : deleted_instances) { - release_resources(del.first); - response.add_purged_instances(del.first); + const auto& name = del.first; + release_resources(name); + response.add_purged_instances(name); + mpl::log(mpl::Level::debug, category, fmt::format("Instance purged: {}", name)); } deleted_instances.clear(); @@ -1544,119 +1673,71 @@ try // clang-format on mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), *config->logger, server}; InfoReply response; + InstanceSnapshotsMap instance_snapshots_map; bool have_mounts = false; bool deleted = false; - auto fetch_info = [&](VirtualMachine& vm) { - const auto& name = vm.vm_name; - auto info = response.add_info(); - auto present_state = vm.current_state(); - info->set_name(name); - if (deleted) - { - info->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); - } - else - { - info->mutable_instance_status()->set_status(grpc_instance_status_for(present_state)); - } + bool snapshots_only = request->snapshots(); + response.set_snapshots(snapshots_only); - auto vm_image = fetch_image_for(name, config->factory->fetch_type(), *config->vault); - auto original_release = vm_image.original_release; - - if (!vm_image.id.empty() && original_release.empty()) + auto process_snapshot_pick = [&response, &have_mounts, snapshots_only](VirtualMachine& vm, + const SnapshotPick& snapshot_pick) { + for (const auto& snapshot_name : snapshot_pick.pick) { - try - { - auto vm_image_info = config->image_hosts.back()->info_for_full_hash(vm_image.id); - original_release = vm_image_info.release_title.toStdString(); - } - catch (const std::exception& e) - { - mpl::log(mpl::Level::warning, category, fmt::format("Cannot fetch image information: {}", e.what())); - } + const auto snapshot = vm.get_snapshot(snapshot_name); // verify validity even if unused + if (!snapshot_pick.all_or_none || !snapshots_only) + populate_snapshot_info(vm, snapshot, response, have_mounts); } + }; - info->set_image_release(original_release); - info->set_id(vm_image.id); - - auto vm_specs = vm_instance_specs[name]; - - auto mount_info = info->mutable_mount_info(); - - mount_info->set_longest_path_len(0); + auto fetch_detailed_report = [this, + &instance_snapshots_map, + process_snapshot_pick, + snapshots_only, + request, + &response, + &have_mounts, + &deleted](VirtualMachine& vm) { + fmt::memory_buffer errors; + const auto& name = vm.vm_name; - if (!vm_specs.mounts.empty()) - have_mounts = true; + const auto& it = instance_snapshots_map.find(name); + const auto& snapshot_pick = it == instance_snapshots_map.end() ? SnapshotPick{{}, true} : it->second; - if (MP_SETTINGS.get_as(mp::mounts_key)) + try { - for (const auto& mount : vm_specs.mounts) + process_snapshot_pick(vm, snapshot_pick); + if (snapshot_pick.all_or_none) { - if (mount.second.source_path.size() > mount_info->longest_path_len()) - { - mount_info->set_longest_path_len(mount.second.source_path.size()); - } - - auto entry = mount_info->add_mount_paths(); - entry->set_source_path(mount.second.source_path); - entry->set_target_path(mount.first); - - for (const auto& uid_mapping : mount.second.uid_mappings) - { - auto uid_pair = entry->mutable_mount_maps()->add_uid_mappings(); - uid_pair->set_host_id(uid_mapping.first); - uid_pair->set_instance_id(uid_mapping.second); - } - for (const auto& gid_mapping : mount.second.gid_mappings) - { - auto gid_pair = entry->mutable_mount_maps()->add_gid_mappings(); - gid_pair->set_host_id(gid_mapping.first); - gid_pair->set_instance_id(gid_mapping.second); - } + if (snapshots_only) + for (const auto& snapshot : vm.view_snapshots()) + populate_snapshot_info(vm, snapshot, response, have_mounts); + else + populate_instance_info(vm, response, request->no_runtime_information(), deleted, have_mounts); } } - - if (!request->no_runtime_information() && mp::utils::is_running(present_state)) + catch (const NoSuchSnapshotException& e) { - mp::SSHSession session{vm.ssh_hostname(), vm.ssh_port(), vm_specs.ssh_username, *config->ssh_key_provider}; - - info->set_load(mpu::run_in_ssh_session(session, "cat /proc/loadavg | cut -d ' ' -f1-3")); - info->set_memory_usage(mpu::run_in_ssh_session(session, "free -b | grep 'Mem:' | awk '{printf $3}'")); - info->set_memory_total(mpu::run_in_ssh_session(session, "free -b | grep 'Mem:' | awk '{printf $2}'")); - info->set_disk_usage( - mpu::run_in_ssh_session(session, "df -t ext4 -t vfat --total -B1 --output=used | tail -n 1")); - info->set_disk_total( - mpu::run_in_ssh_session(session, "df -t ext4 -t vfat --total -B1 --output=size | tail -n 1")); - info->set_cpu_count(mpu::run_in_ssh_session(session, "nproc")); - - std::string management_ip = vm.management_ipv4(*config->ssh_key_provider); - auto all_ipv4 = vm.get_all_ipv4(*config->ssh_key_provider); - - if (is_ipv4_valid(management_ip)) - info->add_ipv4(management_ip); - else if (all_ipv4.empty()) - info->add_ipv4("N/A"); - - for (const auto& extra_ipv4 : all_ipv4) - if (extra_ipv4 != management_ip) - info->add_ipv4(extra_ipv4); - - auto current_release = - mpu::run_in_ssh_session(session, "cat /etc/os-release | grep 'PRETTY_NAME' | cut -d \\\" -f2"); - info->set_current_release(!current_release.empty() ? current_release : original_release); + add_fmt_to(errors, e.what()); } - return grpc::Status::OK; + + return grpc_status_for(errors); }; - auto [instance_selection, status] = - select_instances_and_react(operative_instances, deleted_instances, request->instance_names().instance_name(), - InstanceGroup::All, require_existing_instances_reaction); + auto [instance_selection, status] = select_instances_and_react(operative_instances, + deleted_instances, + request->instance_snapshot_pairs(), + InstanceGroup::All, + require_existing_instances_reaction); if (status.ok()) { - cmd_vms(instance_selection.operative_selection, fetch_info); - deleted = true; - cmd_vms(instance_selection.deleted_selection, fetch_info); + instance_snapshots_map = map_snapshots_to_instances(request->instance_snapshot_pairs()); + + if ((status = cmd_vms(instance_selection.operative_selection, fetch_detailed_report)).ok()) + { + deleted = true; + status = cmd_vms(instance_selection.deleted_selection, fetch_detailed_report); + } if (have_mounts && !MP_SETTINGS.get_as(mp::mounts_key)) mpl::log(mpl::Level::error, category, "Mounts have been disabled on this instance of Multipass"); @@ -1680,17 +1761,22 @@ try // clang-format on ListReply response; config->update_prompt->populate_if_time_to_show(response.mutable_update_info()); - for (const auto& instance : operative_instances) - { - const auto& name = instance.first; - const auto& vm = instance.second; - auto present_state = vm->current_state(); - auto entry = response.add_instances(); + // Need to 'touch' a report in the response so formatters know what to do with an otherwise empty response + request->snapshots() ? (void)response.mutable_snapshot_list() : (void)response.mutable_instance_list(); + bool deleted = false; + + auto fetch_instance = [this, request, &response, &deleted](VirtualMachine& vm) { + const auto& name = vm.vm_name; + auto present_state = vm.current_state(); + auto entry = response.mutable_instance_list()->add_instances(); entry->set_name(name); - entry->mutable_instance_status()->set_status(grpc_instance_status_for(present_state)); + if (deleted) + entry->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); + else + entry->mutable_instance_status()->set_status(grpc_instance_status_for(present_state)); // FIXME: Set the release to the cached current version when supported - auto vm_image = fetch_image_for(name, config->factory->fetch_type(), *config->vault); + auto vm_image = fetch_image_for(name, *config->factory, *config->vault); auto current_release = vm_image.original_release; if (!vm_image.id.empty() && current_release.empty()) @@ -1710,8 +1796,8 @@ try // clang-format on if (request->request_ipv4() && mp::utils::is_running(present_state)) { - std::string management_ip = vm->management_ipv4(*config->ssh_key_provider); - auto all_ipv4 = vm->get_all_ipv4(*config->ssh_key_provider); + std::string management_ip = vm.management_ipv4(*config->ssh_key_provider); + auto all_ipv4 = vm.get_all_ipv4(*config->ssh_key_provider); if (is_ipv4_valid(management_ip)) entry->add_ipv4(management_ip); @@ -1722,18 +1808,44 @@ try // clang-format on if (extra_ipv4 != management_ip) entry->add_ipv4(extra_ipv4); } - } - for (const auto& instance : deleted_instances) + return grpc::Status::OK; + }; + + auto fetch_snapshot = [&response](VirtualMachine& vm) { + fmt::memory_buffer errors; + const auto& name = vm.vm_name; + + try + { + for (const auto& snapshot : vm.view_snapshots()) + { + auto entry = response.mutable_snapshot_list()->add_snapshots(); + auto fundamentals = entry->mutable_fundamentals(); + + entry->set_name(name); + populate_snapshot_fundamentals(snapshot, fundamentals); + } + } + catch (const NoSuchSnapshotException& e) + { + add_fmt_to(errors, e.what()); + } + + return grpc_status_for(errors); + }; + + auto cmd = request->snapshots() ? std::function(fetch_snapshot) : std::function(fetch_instance); + + auto status = cmd_vms(select_all(operative_instances), cmd); + if (status.ok() && !request->snapshots()) { - const auto& name = instance.first; - auto entry = response.add_instances(); - entry->set_name(name); - entry->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); + deleted = true; + status = cmd_vms(select_all(deleted_instances), cmd); } server->Write(response); - status_promise->set_value(grpc::Status::OK); + status_promise->set_value(status); } catch (const std::exception& e) { @@ -1880,6 +1992,7 @@ try // clang-format on operative_instances[name] = std::move(vm_it->second); deleted_instances.erase(vm_it); init_mounts(name); + mpl::log(mpl::Level::debug, category, fmt::format("Instance recovered: {}", name)); } persist_instances(); } @@ -2106,53 +2219,44 @@ try // clang-format on server}; DeleteReply response; - auto [instance_selection, status] = - select_instances_and_react(operative_instances, deleted_instances, request->instance_names().instance_name(), - InstanceGroup::All, require_existing_instances_reaction); + auto [instance_selection, status] = select_instances_and_react(operative_instances, + deleted_instances, + request->instance_snapshot_pairs(), + InstanceGroup::All, + require_existing_instances_reaction); if (status.ok()) { const bool purge = request->purge(); + auto instances_dirty = false; - for (const auto& vm_it : instance_selection.operative_selection) - { - const auto& name = vm_it->first; - auto& instance = vm_it->second; - assert(!vm_instance_specs[name].deleted); - - if (instance->current_state() == VirtualMachine::State::delayed_shutdown) - delayed_shutdown_instances.erase(name); - - mounts[name].clear(); - instance->shutdown(); + auto instance_snapshots_map = map_snapshots_to_instances(request->instance_snapshot_pairs()); + verify_snapshot_picks(instance_selection, instance_snapshots_map); // avoid deleting if any snapshot is missing - if (purge) - { - release_resources(name); - response.add_purged_instances(name); - } - else - { - deleted_instances[name] = std::move(instance); - vm_instance_specs[name].deleted = true; - } - - operative_instances.erase(vm_it); - } - - if (purge) + // start with deleted instances, to avoid iterator invalidation when moving instances there + for (const auto& selection : {instance_selection.deleted_selection, instance_selection.operative_selection}) { - for (const auto& vm_it : instance_selection.deleted_selection) + for (const auto& vm_it : selection) { - const auto& name = vm_it->first; - assert(vm_instance_specs[name].deleted); - response.add_purged_instances(name); - release_resources(name); - deleted_instances.erase(vm_it); + const auto& instance_name = vm_it->first; + + auto snapshot_pick_it = instance_snapshots_map.find(instance_name); + const auto& [pick, all] = snapshot_pick_it == instance_snapshots_map.end() ? SnapshotPick{{}, true} + : snapshot_pick_it->second; + if (all) // we're asked to delete the VM + instances_dirty |= delete_vm(vm_it, purge, response); + else // we're asked to delete snapshots + { + assert(purge && "precondition: snapshots can only be purged"); + + for (const auto& snapshot_name : pick) + vm_it->second->delete_snapshot(snapshot_name); + } } } - persist_instances(); + if (instances_dirty) + persist_instances(); } server->Write(response); @@ -2347,6 +2451,140 @@ catch (const std::exception& e) status_promise->set_value(grpc::Status(grpc::StatusCode::INTERNAL, e.what(), "")); } +void mp::Daemon::snapshot(const mp::SnapshotRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise) +try +{ + mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), + *config->logger, + server}; + + const auto& instance_name = request->instance(); + auto [instance_trail, status] = find_instance_and_react(operative_instances, + deleted_instances, + instance_name, + require_operative_instances_reaction); + + if (status.ok()) + { + assert(instance_trail.index() == 0); + auto* vm_ptr = std::get<0>(instance_trail)->second.get(); + assert(vm_ptr); + + using St = VirtualMachine::State; + if (auto state = vm_ptr->current_state(); state != St::off && state != St::stopped) + return status_promise->set_value( + grpc::Status{grpc::INVALID_ARGUMENT, "Multipass can only take snapshots of stopped instances."}); + + auto snapshot_name = request->snapshot(); + if (!snapshot_name.empty() && !mp::utils::valid_hostname(snapshot_name)) + return status_promise->set_value( + grpc::Status{grpc::INVALID_ARGUMENT, fmt::format(R"(Invalid snapshot name: "{}".)", snapshot_name)}); + + const auto spec_it = vm_instance_specs.find(instance_name); + assert(spec_it != vm_instance_specs.end() && "missing instance specs"); + + SnapshotReply reply; + reply.set_snapshot(vm_ptr->take_snapshot(spec_it->second, snapshot_name, request->comment())->get_name()); + + server->Write(reply); + } + + status_promise->set_value(status); +} +catch (const SnapshotNameTakenException& e) +{ + status_promise->set_value(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, e.what(), "")); +} +catch (const std::exception& e) +{ + status_promise->set_value(grpc::Status(grpc::StatusCode::INTERNAL, e.what(), "")); +} + +void mp::Daemon::restore(const mp::RestoreRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise) +try +{ + mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), + *config->logger, + server}; + + RestoreReply reply; + const auto& instance_name = request->instance(); + auto [instance_trail, status] = find_instance_and_react(operative_instances, + deleted_instances, + instance_name, + require_operative_instances_reaction); + + if (status.ok()) + { + assert(instance_trail.index() == 0); + auto* vm_ptr = std::get<0>(instance_trail)->second.get(); + assert(vm_ptr); + + using St = VirtualMachine::State; + if (auto state = vm_ptr->current_state(); state != St::off && state != St::stopped) + return status_promise->set_value( + grpc::Status{grpc::INVALID_ARGUMENT, "Multipass can only restore snapshots of stopped instances."}); + + auto spec_it = vm_instance_specs.find(instance_name); + assert(spec_it != vm_instance_specs.end() && "missing instance specs"); + auto& vm_specs = spec_it->second; + + // Only need to check if the snapshot exists so the result is discarded + vm_ptr->get_snapshot(request->snapshot()); + + if (!request->destructive()) + { + RestoreReply confirm_action{}; + confirm_action.set_confirm_destructive(true); + if (!server->Write(confirm_action)) + throw std::runtime_error("Cannot request confirmation from client. Aborting..."); + + RestoreRequest client_response; + if (!server->Read(&client_response)) + throw std::runtime_error("Cannot get confirmation from client. Aborting..."); + + if (!client_response.destructive()) + { + reply_msg(server, fmt::format("Taking snapshot before restoring {}", instance_name)); + + const auto snapshot = + vm_ptr->take_snapshot(vm_specs, "", fmt::format("Before restoring {}", request->snapshot())); + + reply_msg(server, + fmt::format("Snapshot taken: {}.{}", instance_name, snapshot->get_name()), + /* sticky = */ true); + } + } + + // Actually restore snapshot + reply_msg(server, "Restoring snapshot"); + auto old_specs = vm_specs; + vm_ptr->restore_snapshot(request->snapshot(), vm_specs); + + auto mounts_it = mounts.find(instance_name); + assert(mounts_it != mounts.end() && "uninitialized mounts"); + + if (update_mounts(vm_specs, mounts_it->second, vm_ptr) || vm_specs != old_specs) + persist_instances(); + + server->Write(reply); + } + + status_promise->set_value(status); +} +catch (const mp::NoSuchSnapshotException& e) +{ + status_promise->set_value(grpc::Status{grpc::StatusCode::NOT_FOUND, e.what(), ""}); +} +catch (const std::exception& e) +{ + status_promise->set_value(grpc::Status(grpc::StatusCode::INTERNAL, e.what(), "")); +} + void mp::Daemon::on_shutdown() { } @@ -2405,13 +2643,13 @@ void mp::Daemon::persist_instances() } QDir data_dir{ mp::utils::backend_directory_path(config->data_directory, config->factory->get_backend_directory_name())}; - mp::write_json(instance_records_json, data_dir.filePath(instance_db_name)); + MP_JSONUTILS.write_json(instance_records_json, data_dir.filePath(instance_db_name)); } void mp::Daemon::release_resources(const std::string& instance) { - config->factory->remove_resources_for(instance); config->vault->remove(instance); + config->factory->remove_resources_for(instance); auto spec_it = vm_instance_specs.find(instance); if (spec_it != cend(vm_instance_specs)) @@ -2669,8 +2907,13 @@ void mp::Daemon::create_vm(const CreateRequest* request, if (!vm_desc.image.id.empty()) checksum = vm_desc.image.id; - auto vm_image = config->vault->fetch_image(fetch_type, query, prepare_action, progress_monitor, - launch_from_blueprint, checksum); + auto vm_image = config->vault->fetch_image(fetch_type, + query, + prepare_action, + progress_monitor, + launch_from_blueprint, + checksum, + config->factory->get_instance_directory(name)); const auto image_size = config->vault->minimum_image_size_for(vm_image.id); vm_desc.disk_space = compute_final_image_size( @@ -2726,6 +2969,49 @@ void mp::Daemon::create_vm(const CreateRequest* request, prepare_future_watcher->setFuture(QtConcurrent::run(make_vm_description)); } +bool mp::Daemon::delete_vm(InstanceTable::iterator vm_it, bool purge, DeleteReply& response) +{ + auto& [name, instance] = *vm_it; + auto* erase_from = purge ? &deleted_instances : nullptr; // to begin with + auto instances_dirty = false; + + if (!vm_instance_specs[name].deleted) + { + mpl::log(mpl::Level::debug, category, fmt::format("Deleting instance: {}", name)); + erase_from = &operative_instances; + if (instance->current_state() == VirtualMachine::State::delayed_shutdown) + delayed_shutdown_instances.erase(name); + + mounts[name].clear(); + instance->shutdown(); + + if (!purge) + { + vm_instance_specs[name].deleted = true; + deleted_instances[name] = std::move(instance); + + instances_dirty = true; + mpl::log(mpl::Level::debug, category, fmt::format("Instance deleted: {}", name)); + } + } + else + mpl::log(mpl::Level::debug, category, fmt::format("Instance is already deleted: {}", name)); + + if (purge) + { + response.add_purged_instances(name); + release_resources(name); + + instances_dirty = true; + mpl::log(mpl::Level::debug, category, fmt::format("Instance purged: {}", name)); + } + + if (erase_from) + erase_from->erase(vm_it); + + return instances_dirty; +} + grpc::Status mp::Daemon::reboot_vm(VirtualMachine& vm) { if (vm.state == VirtualMachine::State::delayed_shutdown) @@ -2820,27 +3106,8 @@ void mp::Daemon::init_mounts(const std::string& name) { auto& vm_mounts = mounts[name]; auto& vm_spec_mounts = vm_instance_specs[name].mounts; - std::vector mounts_to_remove; - for (const auto& [target, vm_mount] : vm_spec_mounts) - { - if (vm_mounts.find(target) == vm_mounts.end()) - try - { - vm_mounts[target] = make_mount(operative_instances[name].get(), target, vm_mount); - } - catch (const std::exception& e) - { - mpl::log(mpl::Level::warning, category, - fmt::format(R"(Removing mount "{}" => "{}" from '{}': {})", vm_mount.source_path, target, name, - e.what())); - mounts_to_remove.push_back(target); - } - } - for (const auto& mount_target : mounts_to_remove) - vm_spec_mounts.erase(mount_target); - - if (!mounts_to_remove.empty()) + if (!create_missing_mounts(vm_spec_mounts, vm_mounts, operative_instances[name].get())) persist_instances(); } @@ -2855,6 +3122,50 @@ void mp::Daemon::stop_mounts(const std::string& name) } } +bool mp::Daemon::update_mounts(mp::VMSpecs& vm_specs, + std::unordered_map& vm_mounts, + mp::VirtualMachine* vm) +{ + auto& mount_specs = vm_specs.mounts; + return prune_obsolete_mounts(mount_specs, vm_mounts) || !create_missing_mounts(mount_specs, vm_mounts, vm); +} + +bool mp::Daemon::create_missing_mounts(std::unordered_map& mount_specs, + std::unordered_map& vm_mounts, + mp::VirtualMachine* vm) +{ + auto initial_mount_count = mount_specs.size(); + auto specs_it = mount_specs.begin(); + while (specs_it != mount_specs.end()) // TODO@C++20 replace with erase_if over mount_specs + { + const auto& [target, mount_spec] = *specs_it; + if (vm_mounts.find(target) == vm_mounts.end()) + { + try + { + vm_mounts[target] = make_mount(vm, target, mount_spec); + } + catch (const std::exception& e) + { + mpl::log(mpl::Level::warning, + category, + fmt::format(R"(Removing mount "{}" => "{}" from '{}': {})", + mount_spec.source_path, + target, + vm->vm_name, + e.what())); + + specs_it = mount_specs.erase(specs_it); // unordered_map so only iterators to erased element invalidated + continue; + } + } + ++specs_it; + } + + assert(mount_specs.size() <= initial_mount_count); + return mount_specs.size() != initial_mount_count; +} + mp::MountHandler::UPtr mp::Daemon::make_mount(VirtualMachine* vm, const std::string& target, const VMMount& mount) { return mount.mount_type == VMMount::MountType::Classic @@ -3044,3 +3355,89 @@ void mp::Daemon::wait_update_manifests_all_and_optionally_applied_force(const bo update_manifests_all_task.start_timer(); } } + +template +void mp::Daemon::reply_msg(grpc::ServerReaderWriterInterface* server, std::string&& msg, bool sticky) +{ + Reply reply{}; + if (sticky) + reply.set_reply_message(fmt::format("{}\n", std::forward(msg))); + else + reply.set_reply_message(std::forward(msg)); + + server->Write(reply); +} + +void mp::Daemon::populate_instance_info(VirtualMachine& vm, + mp::InfoReply& response, + bool no_runtime_info, + bool deleted, + bool& have_mounts) +{ + auto* info = response.add_details(); + auto instance_info = info->mutable_instance_info(); + auto present_state = vm.current_state(); + + const auto& name = vm.vm_name; + info->set_name(name); + + if (deleted) + info->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); + else + info->mutable_instance_status()->set_status(grpc_instance_status_for(present_state)); + + auto vm_image = fetch_image_for(name, *config->factory, *config->vault); + auto original_release = vm_image.original_release; + + if (!vm_image.id.empty() && original_release.empty()) + { + try + { + auto vm_image_info = config->image_hosts.back()->info_for_full_hash(vm_image.id); + original_release = vm_image_info.release_title.toStdString(); + } + catch (const std::exception& e) + { + mpl::log(mpl::Level::warning, category, fmt::format("Cannot fetch image information: {}", e.what())); + } + } + + instance_info->set_num_snapshots(vm.get_num_snapshots()); + instance_info->set_image_release(original_release); + instance_info->set_id(vm_image.id); + + auto vm_specs = vm_instance_specs[name]; + + auto mount_info = info->mutable_mount_info(); + populate_mount_info(vm_specs.mounts, mount_info, have_mounts); + + if (!no_runtime_info && mp::utils::is_running(present_state)) + { + mp::SSHSession session{vm.ssh_hostname(), vm.ssh_port(), vm_specs.ssh_username, *config->ssh_key_provider}; + + instance_info->set_load(mpu::run_in_ssh_session(session, "cat /proc/loadavg | cut -d ' ' -f1-3")); + instance_info->set_memory_usage(mpu::run_in_ssh_session(session, "free -b | grep 'Mem:' | awk '{printf $3}'")); + info->set_memory_total(mpu::run_in_ssh_session(session, "free -b | grep 'Mem:' | awk '{printf $2}'")); + instance_info->set_disk_usage( + mpu::run_in_ssh_session(session, "df -t ext4 -t vfat --total -B1 --output=used | tail -n 1")); + info->set_disk_total( + mpu::run_in_ssh_session(session, "df -t ext4 -t vfat --total -B1 --output=size | tail -n 1")); + info->set_cpu_count(mpu::run_in_ssh_session(session, "nproc")); + + std::string management_ip = vm.management_ipv4(*config->ssh_key_provider); + auto all_ipv4 = vm.get_all_ipv4(*config->ssh_key_provider); + + if (is_ipv4_valid(management_ip)) + instance_info->add_ipv4(management_ip); + else if (all_ipv4.empty()) + instance_info->add_ipv4("N/A"); + + for (const auto& extra_ipv4 : all_ipv4) + if (extra_ipv4 != management_ip) + instance_info->add_ipv4(extra_ipv4); + + auto current_release = + mpu::run_in_ssh_session(session, "cat /etc/os-release | grep 'PRETTY_NAME' | cut -d \\\" -f2"); + instance_info->set_current_release(!current_release.empty() ? current_release : original_release); + } +} diff --git a/src/daemon/daemon.h b/src/daemon/daemon.h index 7a9f6a2c069..790b1c87fae 100644 --- a/src/daemon/daemon.h +++ b/src/daemon/daemon.h @@ -20,12 +20,12 @@ #include "daemon_config.h" #include "daemon_rpc.h" -#include "vm_specs.h" #include #include #include #include +#include #include #include @@ -42,6 +42,7 @@ namespace multipass { struct DaemonConfig; class SettingsHandler; + class Daemon : public QObject, public multipass::VMStatusMonitor { Q_OBJECT @@ -52,6 +53,8 @@ class Daemon : public QObject, public multipass::VMStatusMonitor void persist_instances(); protected: + using InstanceTable = std::unordered_map; + void on_resume() override; void on_stop() override; void on_shutdown() override; @@ -136,16 +139,37 @@ public slots: grpc::ServerReaderWriterInterface* server, std::promise* status_promise); + virtual void snapshot(const SnapshotRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise); + + virtual void restore(const RestoreRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise); + private: void release_resources(const std::string& instance); void create_vm(const CreateRequest* request, grpc::ServerReaderWriterInterface* server, std::promise* status_promise, bool start); + bool delete_vm(InstanceTable::iterator vm_it, bool purge, DeleteReply& response); grpc::Status reboot_vm(VirtualMachine& vm); grpc::Status shutdown_vm(VirtualMachine& vm, const std::chrono::milliseconds delay); grpc::Status cancel_vm_shutdown(const VirtualMachine& vm); grpc::Status get_ssh_info_for_vm(VirtualMachine& vm, SSHInfoReply& response); + void init_mounts(const std::string& name); void stop_mounts(const std::string& name); + + // This returns whether any specs were updated (and need persisting) + bool update_mounts(VMSpecs& vm_specs, + std::unordered_map& vm_mounts, + VirtualMachine* vm); + + // This returns whether all required mount handlers were successfully created + bool create_missing_mounts(std::unordered_map& mount_specs, + std::unordered_map& vm_mounts, + VirtualMachine* vm); + MountHandler::UPtr make_mount(VirtualMachine* vm, const std::string& target, const VMMount& mount); struct AsyncOperationStatus @@ -169,10 +193,16 @@ public slots: // it is applied in Daemon::find wherever the image info fetching is involved, aka non-only-blueprints case void wait_update_manifests_all_and_optionally_applied_force(const bool force_manifest_network_download); + template + void reply_msg(grpc::ServerReaderWriterInterface* server, std::string&& msg, bool sticky = false); + + void + populate_instance_info(VirtualMachine& vm, InfoReply& response, bool runtime_info, bool deleted, bool& have_mounts); + std::unique_ptr config; std::unordered_map vm_instance_specs; - std::unordered_map operative_instances; - std::unordered_map deleted_instances; + InstanceTable operative_instances; + InstanceTable deleted_instances; std::unordered_map> delayed_shutdown_instances; std::unordered_set allocated_mac_addrs; DaemonRpc daemon_rpc; @@ -185,6 +215,7 @@ public slots: std::unordered_set preparing_instances; QFuture image_update_future; SettingsHandler* instance_mod_handler; + SettingsHandler* snapshot_mod_handler; std::unordered_map> mounts; }; } // namespace multipass diff --git a/src/daemon/daemon_rpc.cpp b/src/daemon/daemon_rpc.cpp index 7a63b891e25..ecfd56e6764 100644 --- a/src/daemon/daemon_rpc.cpp +++ b/src/daemon/daemon_rpc.cpp @@ -366,6 +366,28 @@ grpc::Status mp::DaemonRpc::keys(grpc::ServerContext* context, grpc::ServerReade std::bind(&DaemonRpc::on_keys, this, &request, server, std::placeholders::_1), client_cert_from(context)); } +grpc::Status mp::DaemonRpc::snapshot(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) +{ + SnapshotRequest request; + server->Read(&request); + + return verify_client_and_dispatch_operation( + std::bind(&DaemonRpc::on_snapshot, this, &request, server, std::placeholders::_1), + client_cert_from(context)); +} + +grpc::Status mp::DaemonRpc::restore(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) +{ + RestoreRequest request; + server->Read(&request); + + return verify_client_and_dispatch_operation( + std::bind(&DaemonRpc::on_restore, this, &request, server, std::placeholders::_1), + client_cert_from(context)); +} + template grpc::Status mp::DaemonRpc::verify_client_and_dispatch_operation(OperationSignal signal, const std::string& client_cert) { diff --git a/src/daemon/daemon_rpc.h b/src/daemon/daemon_rpc.h index af1d6890dd9..0e5c979a69e 100644 --- a/src/daemon/daemon_rpc.h +++ b/src/daemon/daemon_rpc.h @@ -98,6 +98,12 @@ class DaemonRpc : public QObject, public multipass::Rpc::Service, private Disabl void on_authenticate(const AuthenticateRequest* request, grpc::ServerReaderWriter* server, std::promise* status_promise); + void on_snapshot(const SnapshotRequest* request, + grpc::ServerReaderWriter* server, + std::promise* status_promise); + void on_restore(const RestoreRequest* request, + grpc::ServerReaderWriter* server, + std::promise* status_promise); private: template @@ -145,6 +151,10 @@ class DaemonRpc : public QObject, public multipass::Rpc::Service, private Disabl grpc::Status keys(grpc::ServerContext* context, grpc::ServerReaderWriter* server) override; grpc::Status authenticate(grpc::ServerContext* context, grpc::ServerReaderWriter* server) override; + grpc::Status snapshot(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) override; + grpc::Status restore(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) override; }; } // namespace multipass #endif // MULTIPASS_DAEMON_RPC_H diff --git a/src/daemon/default_vm_image_vault.cpp b/src/daemon/default_vm_image_vault.cpp index 85be7bf1836..cdf711c6fa5 100644 --- a/src/daemon/default_vm_image_vault.cpp +++ b/src/daemon/default_vm_image_vault.cpp @@ -218,15 +218,29 @@ mp::MemorySize get_image_size(const mp::Path& image_path) return image_size; } + +template +void persist_records(const T& records, const QString& path) +{ + QJsonObject json_records; + for (const auto& record : records) + { + auto key = QString::fromStdString(record.first); + json_records.insert(key, record_to_json(record.second)); + } + MP_JSONUTILS.write_json(json_records, path); +} } // namespace -mp::DefaultVMImageVault::DefaultVMImageVault(std::vector image_hosts, URLDownloader* downloader, - mp::Path cache_dir_path, mp::Path data_dir_path, mp::days days_to_expire) +mp::DefaultVMImageVault::DefaultVMImageVault(std::vector image_hosts, + URLDownloader* downloader, + const mp::Path& cache_dir_path, + const mp::Path& data_dir_path, + const mp::days& days_to_expire) : BaseVMImageVault{image_hosts}, url_downloader{downloader}, cache_dir{QDir(cache_dir_path).filePath("vault")}, data_dir{QDir(data_dir_path).filePath("vault")}, - instances_dir(data_dir.filePath("instances")), images_dir(cache_dir.filePath("images")), days_to_expire{days_to_expire}, prepared_image_records{load_db(cache_dir.filePath(image_db_name))}, @@ -239,9 +253,13 @@ mp::DefaultVMImageVault::~DefaultVMImageVault() url_downloader->abort_all_downloads(); } -mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, const Query& query, - const PrepareAction& prepare, const ProgressMonitor& monitor, - const bool unlock, const std::optional& checksum) +mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, + const Query& query, + const PrepareAction& prepare, + const ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const mp::Path& save_dir) { { std::lock_guard lock{fetch_mutex}; @@ -269,11 +287,11 @@ mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, co if (source_image.image_path.endsWith(".xz")) { - source_image.image_path = extract_image_from(query.name, source_image, monitor); + source_image.image_path = extract_image_from(source_image, monitor, save_dir); } else { - source_image = image_instance_from(query.name, source_image); + source_image = image_instance_from(source_image, save_dir); } vm_image = prepare(source_image); @@ -314,7 +332,7 @@ mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, co if (last_modified.isValid() && (last_modified.toString().toStdString() == record.image.release_date)) { - return finalize_image_records(query, record.image, id); + return finalize_image_records(query, record.image, id, save_dir); } } @@ -376,7 +394,7 @@ mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, co const auto prepared_image = record.second.image; try { - return finalize_image_records(query, prepared_image, record.first); + return finalize_image_records(query, prepared_image, record.first, save_dir); } catch (const std::exception& e) { @@ -414,7 +432,7 @@ mp::VMImage mp::DefaultVMImageVault::fetch_image(const FetchType& fetch_type, co auto prepared_image = future.result(); std::lock_guard lock{fetch_mutex}; in_progress_image_fetches.erase(id); - return finalize_image_records(query, prepared_image, id); + return finalize_image_records(query, prepared_image, id, save_dir); } catch (const std::exception&) { @@ -431,10 +449,6 @@ void mp::DefaultVMImageVault::remove(const std::string& name) if (name_entry == instance_image_records.end()) return; - QDir instance_dir{instances_dir}; - if (instance_dir.cd(QString::fromStdString(name))) - instance_dir.removeRecursively(); - instance_image_records.erase(name); persist_instance_records(); } @@ -523,7 +537,13 @@ void mp::DefaultVMImageVault::update_images(const FetchType& fetch_type, const P mpl::log(mpl::Level::info, category, fmt::format("Updating {} source image to latest", record.query.release)); try { - fetch_image(fetch_type, record.query, prepare, monitor, false, std::nullopt); + fetch_image(fetch_type, + record.query, + prepare, + monitor, + false, + std::nullopt, + QFileInfo{record.image.image_path}.absolutePath()); // Remove old image std::lock_guard lock{fetch_mutex}; @@ -620,25 +640,23 @@ mp::VMImage mp::DefaultVMImageVault::download_and_prepare_source_image( } } -QString mp::DefaultVMImageVault::extract_image_from(const std::string& instance_name, const VMImage& source_image, - const ProgressMonitor& monitor) +QString mp::DefaultVMImageVault::extract_image_from(const VMImage& source_image, + const ProgressMonitor& monitor, + const mp::Path& dest_dir) { - const auto name = QString::fromStdString(instance_name); - const QDir output_dir{MP_UTILS.make_dir(instances_dir, name)}; + MP_UTILS.make_dir(dest_dir); QFileInfo file_info{source_image.image_path}; const auto image_name = file_info.fileName().remove(".xz"); - const auto image_path = output_dir.filePath(image_name); + const auto image_path = QDir(dest_dir).filePath(image_name); return mp::vault::extract_image(image_path, monitor); } -mp::VMImage mp::DefaultVMImageVault::image_instance_from(const std::string& instance_name, - const VMImage& prepared_image) +mp::VMImage mp::DefaultVMImageVault::image_instance_from(const VMImage& prepared_image, const mp::Path& dest_dir) { - auto name = QString::fromStdString(instance_name); - auto output_dir = MP_UTILS.make_dir(instances_dir, name); + MP_UTILS.make_dir(dest_dir); - return {mp::vault::copy(prepared_image.image_path, output_dir), + return {mp::vault::copy(prepared_image.image_path, dest_dir), prepared_image.id, prepared_image.original_release, prepared_image.current_release, @@ -657,14 +675,16 @@ std::optional> mp::DefaultVMImageVault::get_image_future(co return std::nullopt; } -mp::VMImage mp::DefaultVMImageVault::finalize_image_records(const Query& query, const VMImage& prepared_image, - const std::string& id) +mp::VMImage mp::DefaultVMImageVault::finalize_image_records(const Query& query, + const VMImage& prepared_image, + const std::string& id, + const mp::Path& dest_dir) { VMImage vm_image; if (!query.name.empty()) { - vm_image = image_instance_from(query.name, prepared_image); + vm_image = image_instance_from(prepared_image, dest_dir); instance_image_records[query.name] = {vm_image, query, std::chrono::system_clock::now()}; } @@ -679,21 +699,6 @@ mp::VMImage mp::DefaultVMImageVault::finalize_image_records(const Query& query, return vm_image; } -namespace -{ -template -void persist_records(const T& records, const QString& path) -{ - QJsonObject json_records; - for (const auto& record : records) - { - auto key = QString::fromStdString(record.first); - json_records.insert(key, record_to_json(record.second)); - } - mp::write_json(json_records, path); -} -} // namespace - void mp::DefaultVMImageVault::persist_instance_records() { persist_records(instance_image_records, data_dir.filePath(instance_db_name)); diff --git a/src/daemon/default_vm_image_vault.h b/src/daemon/default_vm_image_vault.h index 5a008873705..2719f0420fe 100644 --- a/src/daemon/default_vm_image_vault.h +++ b/src/daemon/default_vm_image_vault.h @@ -45,13 +45,20 @@ class VaultRecord class DefaultVMImageVault final : public BaseVMImageVault { public: - DefaultVMImageVault(std::vector image_host, URLDownloader* downloader, multipass::Path cache_dir_path, - multipass::Path data_dir_path, multipass::days days_to_expire); + DefaultVMImageVault(std::vector image_host, + URLDownloader* downloader, + const multipass::Path& cache_dir_path, + const multipass::Path& data_dir_path, + const multipass::days& days_to_expire); ~DefaultVMImageVault(); - VMImage fetch_image(const FetchType& fetch_type, const Query& query, const PrepareAction& prepare, - const ProgressMonitor& monitor, const bool unlock, - const std::optional& checksum) override; + VMImage fetch_image(const FetchType& fetch_type, + const Query& query, + const PrepareAction& prepare, + const ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const Path& save_dir) override; void remove(const std::string& name) override; bool has_record_for(const std::string& name) override; void prune_expired_images() override; @@ -60,21 +67,22 @@ class DefaultVMImageVault final : public BaseVMImageVault MemorySize minimum_image_size_for(const std::string& id) override; private: - VMImage image_instance_from(const std::string& name, const VMImage& prepared_image); + VMImage image_instance_from(const VMImage& prepared_image, const Path& dest_dir); VMImage download_and_prepare_source_image(const VMImageInfo& info, std::optional& existing_source_image, const QDir& image_dir, const FetchType& fetch_type, const PrepareAction& prepare, const ProgressMonitor& monitor); - QString extract_image_from(const std::string& instance_name, const VMImage& source_image, - const ProgressMonitor& monitor); + QString extract_image_from(const VMImage& source_image, const ProgressMonitor& monitor, const Path& dest_dir); std::optional> get_image_future(const std::string& id); - VMImage finalize_image_records(const Query& query, const VMImage& prepared_image, const std::string& id); + VMImage finalize_image_records(const Query& query, + const VMImage& prepared_image, + const std::string& id, + const Path& dest_dir); void persist_image_records(); void persist_instance_records(); URLDownloader* const url_downloader; const QDir cache_dir; const QDir data_dir; - const QDir instances_dir; const QDir images_dir; const days days_to_expire; std::mutex fetch_mutex; diff --git a/src/daemon/instance_settings_handler.cpp b/src/daemon/instance_settings_handler.cpp index 8c8f54be3e1..3cc2f37c076 100644 --- a/src/daemon/instance_settings_handler.cpp +++ b/src/daemon/instance_settings_handler.cpp @@ -163,11 +163,12 @@ mp::InstanceSettingsException::InstanceSettingsException(const std::string& reas mp::InstanceSettingsHandler::InstanceSettingsHandler( std::unordered_map& vm_instance_specs, - std::unordered_map& vm_instances, + std::unordered_map& operative_instances, const std::unordered_map& deleted_instances, - const std::unordered_set& preparing_instances, std::function instance_persister) + const std::unordered_set& preparing_instances, + std::function instance_persister) : vm_instance_specs{vm_instance_specs}, - vm_instances{vm_instances}, + operative_instances{operative_instances}, deleted_instances{deleted_instances}, preparing_instances{preparing_instances}, instance_persister{std::move(instance_persister)} @@ -181,7 +182,7 @@ std::set mp::InstanceSettingsHandler::keys() const std::set ret; for (const auto& item : vm_instance_specs) for (const auto& suffix : {cpus_suffix, mem_suffix, disk_suffix}) - ret.insert(key_template.arg(item.first.c_str()).arg(suffix)); + ret.insert(key_template.arg(item.first.c_str(), suffix)); return ret; } @@ -206,7 +207,7 @@ void mp::InstanceSettingsHandler::set(const QString& key, const QString& val) auto [instance_name, property] = parse_key(key); if (preparing_instances.find(instance_name) != preparing_instances.end()) - throw InstanceSettingsException{operation_msg(Operation::Modify), instance_name, "Instance is being prepared"}; + throw InstanceSettingsException{operation_msg(Operation::Modify), instance_name, "instance is being prepared"}; auto& instance = modify_instance(instance_name); // we need this first, to refuse updating deleted instances auto& spec = modify_spec(instance_name); @@ -231,7 +232,7 @@ void mp::InstanceSettingsHandler::set(const QString& key, const QString& val) auto mp::InstanceSettingsHandler::modify_instance(const std::string& instance_name) -> VirtualMachine& { - auto ret = pick_instance(vm_instances, instance_name, Operation::Modify, deleted_instances); + auto ret = pick_instance(operative_instances, instance_name, Operation::Modify, deleted_instances); assert(ret && "can't have null instance"); return *ret; diff --git a/src/daemon/instance_settings_handler.h b/src/daemon/instance_settings_handler.h index 10f8cc43b72..d2236be533f 100644 --- a/src/daemon/instance_settings_handler.h +++ b/src/daemon/instance_settings_handler.h @@ -18,11 +18,10 @@ #ifndef MULTIPASS_INSTANCE_SETTINGS_HANDLER_H #define MULTIPASS_INSTANCE_SETTINGS_HANDLER_H -#include "vm_specs.h" - #include #include #include +#include #include @@ -34,11 +33,12 @@ namespace multipass { + class InstanceSettingsHandler : public SettingsHandler { public: InstanceSettingsHandler(std::unordered_map& vm_instance_specs, - std::unordered_map& vm_instances, + std::unordered_map& operative_instances, const std::unordered_map& deleted_instances, const std::unordered_set& preparing_instances, std::function instance_persister); @@ -55,7 +55,7 @@ class InstanceSettingsHandler : public SettingsHandler private: // references, careful std::unordered_map& vm_instance_specs; - std::unordered_map& vm_instances; + std::unordered_map& operative_instances; const std::unordered_map& deleted_instances; const std::unordered_set& preparing_instances; std::function instance_persister; diff --git a/src/daemon/snapshot_settings_handler.cpp b/src/daemon/snapshot_settings_handler.cpp new file mode 100644 index 00000000000..0af41669a76 --- /dev/null +++ b/src/daemon/snapshot_settings_handler.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "snapshot_settings_handler.h" +#include "multipass/exceptions/snapshot_exceptions.h" +#include "multipass/utils.h" + +#include +#include + +#include + +namespace mp = multipass; + +namespace +{ +constexpr auto name_suffix = "name"; +constexpr auto comment_suffix = "comment"; +constexpr auto common_exception_msg = "Cannot access snapshot settings"; + +QRegularExpression make_key_regex() +{ + const auto instance_pattern = QStringLiteral("(?.+)"); + const auto snapshot_pattern = QStringLiteral("(?.+)"); + + const auto property_template = QStringLiteral("(?%1)"); + const auto either_prop = QStringList{name_suffix, comment_suffix}.join("|"); + const auto property_pattern = property_template.arg(either_prop); + + const auto key_template = QStringLiteral(R"(%1\.%2\.%3\.%4)"); + const auto key_pattern = + key_template.arg(mp::daemon_settings_root, instance_pattern, snapshot_pattern, property_pattern); + + return QRegularExpression{QRegularExpression::anchoredPattern(key_pattern)}; +} + +std::tuple parse_key(const QString& key) +{ + static const auto key_regex = make_key_regex(); + + auto match = key_regex.match(key); + if (match.hasMatch()) + { + auto instance = match.captured("instance"); + auto snapshot = match.captured("snapshot"); + auto property = match.captured("property"); + + assert(!instance.isEmpty() && !snapshot.isEmpty() && !property.isEmpty()); + return {instance.toStdString(), snapshot, property.toStdString()}; + } + + throw mp::UnrecognizedSettingException{key}; +} +} // namespace + +mp::SnapshotSettingsException::SnapshotSettingsException(const std::string& missing_instance, const std::string& detail) + : SettingsException{fmt::format("{}; instance: {}; reason: {}", common_exception_msg, missing_instance, detail)} +{ +} + +mp::SnapshotSettingsException::SnapshotSettingsException(const std::string& detail) + : SettingsException{fmt::format("{}; reason: {}", common_exception_msg, detail)} +{ +} + +mp::SnapshotSettingsHandler::SnapshotSettingsHandler( + std::unordered_map& operative_instances, + const std::unordered_map& deleted_instances, + const std::unordered_set& preparing_instances) noexcept + : operative_instances{operative_instances}, + deleted_instances{deleted_instances}, + preparing_instances{preparing_instances} +{ +} + +std::set mp::SnapshotSettingsHandler::keys() const +{ + static const auto key_template = QStringLiteral("%1.%2.%3.%4").arg(daemon_settings_root); + std::set ret; + + const auto& const_operative_instances = operative_instances; + for (const auto* instance_map : {&const_operative_instances, &deleted_instances}) + for (const auto& [vm_name, vm] : *instance_map) + for (const auto& snapshot : vm->view_snapshots()) + for (const auto& suffix : {name_suffix, comment_suffix}) + ret.insert(key_template.arg(vm_name.c_str(), snapshot->get_name().c_str(), suffix)); + + return ret; +} + +QString mp::SnapshotSettingsHandler::get(const QString& key) const +{ + auto [instance_name, snapshot_name, property] = parse_key(key); + + auto snapshot = find_snapshot(instance_name, snapshot_name.toStdString()); + + if (property == name_suffix) + return snapshot_name; // not very useful, but for completeness + + assert(property == comment_suffix); + return QString::fromStdString(snapshot->get_comment()); +} + +void mp::SnapshotSettingsHandler::set(const QString& key, const QString& val) +{ + auto [instance_name, snapshot_name, property] = parse_key(key); + auto snapshot_name_stdstr = snapshot_name.toStdString(); + auto val_stdstr = val.toStdString(); + + if (property == name_suffix) + { + if (snapshot_name == val) + { + find_snapshot(instance_name, snapshot_name_stdstr); // fail if it ain't there + return; + } + + if (val_stdstr.empty() || !mp::utils::valid_hostname(val_stdstr)) + throw mp::InvalidSettingException{key, val, "Invalid snapshot name."}; + + modify_instance(instance_name)->rename_snapshot(snapshot_name_stdstr, val_stdstr); + } + else + { + assert(property == comment_suffix); + auto snapshot = modify_snapshot(instance_name, snapshot_name_stdstr); + snapshot->set_comment(val_stdstr); + } +} + +auto mp::SnapshotSettingsHandler::find_snapshot(const std::string& instance_name, + const std::string& snapshot_name) const + -> std::shared_ptr +{ + try + { + return find_instance(instance_name)->get_snapshot(snapshot_name); + } + catch (const NoSuchSnapshotException& e) + { + throw SnapshotSettingsException{e.what()}; + } +} + +auto mp::SnapshotSettingsHandler::find_instance(const std::string& instance_name, bool deleted_ok) const + -> std::shared_ptr +{ + if (preparing_instances.find(instance_name) != preparing_instances.end()) + throw SnapshotSettingsException{instance_name, "instance is being prepared"}; + + try + { + return operative_instances.at(instance_name); + } + catch (std::out_of_range&) + { + std::string error{"No such instance"}; + + try + { + const auto& del = deleted_instances.at(instance_name); + if (deleted_ok) + return del; + + error = "Instance is deleted"; + } + catch (std::out_of_range&) + { + } + + throw SnapshotSettingsException{instance_name, error}; + } +} + +auto mp::SnapshotSettingsHandler::modify_instance(const std::string& instance_name) -> std::shared_ptr +{ + return std::const_pointer_cast(find_instance(instance_name, false)); +} + +auto mp::SnapshotSettingsHandler::modify_snapshot(const std::string& instance_name, const std::string& snapshot_name) + -> std::shared_ptr +{ + return std::const_pointer_cast(find_snapshot(instance_name, snapshot_name)); +} diff --git a/src/daemon/snapshot_settings_handler.h b/src/daemon/snapshot_settings_handler.h new file mode 100644 index 00000000000..c613bbc6cad --- /dev/null +++ b/src/daemon/snapshot_settings_handler.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_SNAPSHOT_SETTINGS_HANDLER_H +#define MULTIPASS_SNAPSHOT_SETTINGS_HANDLER_H + +#include +#include +#include +#include + +#include +#include +#include + +namespace multipass +{ + +class SnapshotSettingsHandler : public SettingsHandler +{ +public: + SnapshotSettingsHandler(std::unordered_map& operative_instances, + const std::unordered_map& deleted_instances, + const std::unordered_set& preparing_instances) noexcept; + + std::set keys() const override; + QString get(const QString& key) const override; + void set(const QString& key, const QString& val) override; + +private: + std::shared_ptr find_snapshot(const std::string& instance_name, + const std::string& snapshot_name) const; + std::shared_ptr find_instance(const std::string& instance_name, bool deleted_ok = true) const; + + std::shared_ptr modify_snapshot(const std::string& instance_name, const std::string& snapshot_name); + std::shared_ptr modify_instance(const std::string& instance_name); + +private: + // references, careful + std::unordered_map& operative_instances; + const std::unordered_map& deleted_instances; + const std::unordered_set& preparing_instances; +}; + +class SnapshotSettingsException : public SettingsException +{ +public: + SnapshotSettingsException(const std::string& missing_instance, const std::string& detail); + explicit SnapshotSettingsException(const std::string& detail); +}; + +} // namespace multipass + +#endif // MULTIPASS_SNAPSHOT_SETTINGS_HANDLER_H diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp index 705b53778ef..82c5dd0e296 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp +++ b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp @@ -274,9 +274,11 @@ std::string management_ipv4_impl(std::optional& management_ip, } // namespace mp::LibVirtVirtualMachine::LibVirtVirtualMachine(const mp::VirtualMachineDescription& desc, - const std::string& bridge_name, mp::VMStatusMonitor& monitor, - const mp::LibvirtWrapper::UPtr& libvirt_wrapper) - : BaseVirtualMachine{desc.vm_name}, + const std::string& bridge_name, + mp::VMStatusMonitor& monitor, + const mp::LibvirtWrapper::UPtr& libvirt_wrapper, + const mp::Path& instance_dir) + : BaseVirtualMachine{desc.vm_name, instance_dir}, username{desc.ssh_username}, desc{desc}, monitor{&monitor}, diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine.h b/src/platform/backends/libvirt/libvirt_virtual_machine.h index bf717fc3dd3..4a4d4c1a8c6 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine.h +++ b/src/platform/backends/libvirt/libvirt_virtual_machine.h @@ -35,8 +35,11 @@ class LibVirtVirtualMachine final : public BaseVirtualMachine using DomainUPtr = std::unique_ptr; using NetworkUPtr = std::unique_ptr; - LibVirtVirtualMachine(const VirtualMachineDescription& desc, const std::string& bridge_name, - VMStatusMonitor& monitor, const LibvirtWrapper::UPtr& libvirt_wrapper); + LibVirtVirtualMachine(const VirtualMachineDescription& desc, + const std::string& bridge_name, + VMStatusMonitor& monitor, + const LibvirtWrapper::UPtr& libvirt_wrapper, + const Path& instance_dir); ~LibVirtVirtualMachine(); void start() override; diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp index 358b4f3e955..87b26e4e518 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp +++ b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp @@ -108,7 +108,9 @@ auto make_libvirt_wrapper(const std::string& libvirt_object_path) mp::LibVirtVirtualMachineFactory::LibVirtVirtualMachineFactory(const mp::Path& data_dir, const std::string& libvirt_object_path) - : libvirt_wrapper{make_libvirt_wrapper(libvirt_object_path)}, + : BaseVirtualMachineFactory( + MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)), + libvirt_wrapper{make_libvirt_wrapper(libvirt_object_path)}, data_dir{data_dir}, bridge_name{enable_libvirt_network(data_dir, libvirt_wrapper)}, libvirt_object_path{libvirt_object_path} @@ -126,7 +128,11 @@ mp::VirtualMachine::UPtr mp::LibVirtVirtualMachineFactory::create_virtual_machin if (bridge_name.empty()) bridge_name = enable_libvirt_network(data_dir, libvirt_wrapper); - return std::make_unique(desc, bridge_name, monitor, libvirt_wrapper); + return std::make_unique(desc, + bridge_name, + monitor, + libvirt_wrapper, + get_instance_directory(desc.vm_name)); } mp::LibVirtVirtualMachineFactory::~LibVirtVirtualMachineFactory() @@ -141,7 +147,7 @@ mp::LibVirtVirtualMachineFactory::~LibVirtVirtualMachineFactory() } } -void mp::LibVirtVirtualMachineFactory::remove_resources_for(const std::string& name) +void mp::LibVirtVirtualMachineFactory::remove_resources_for_impl(const std::string& name) { auto connection = LibVirtVirtualMachine::open_libvirt_connection(libvirt_wrapper); @@ -175,7 +181,7 @@ void mp::LibVirtVirtualMachineFactory::hypervisor_health_check() bridge_name = enable_libvirt_network(data_dir, libvirt_wrapper); } -QString mp::LibVirtVirtualMachineFactory::get_backend_version_string() +QString mp::LibVirtVirtualMachineFactory::get_backend_version_string() const { try { diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h index 5959a5eb13b..f26fbc9a9a2 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h +++ b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h @@ -37,15 +37,17 @@ class LibVirtVirtualMachineFactory final : public BaseVirtualMachineFactory VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor) override; - void remove_resources_for(const std::string& name) override; VMImage prepare_source_image(const VMImage& source_image) override; void prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) override; void hypervisor_health_check() override; - QString get_backend_version_string() override; + QString get_backend_version_string() const override; // Making this public makes this modifiable which is necessary for testing LibvirtWrapper::UPtr libvirt_wrapper; +protected: + void remove_resources_for_impl(const std::string& name) override; + private: const Path data_dir; std::string bridge_name; diff --git a/src/platform/backends/lxd/lxd_mount_handler.cpp b/src/platform/backends/lxd/lxd_mount_handler.cpp index c2dc81c2539..cc54779d0a4 100644 --- a/src/platform/backends/lxd/lxd_mount_handler.cpp +++ b/src/platform/backends/lxd/lxd_mount_handler.cpp @@ -18,6 +18,8 @@ #include "lxd_mount_handler.h" #include "lxd_request.h" +namespace mp = multipass; + namespace { constexpr std::string_view category = "lxd-mount-handler"; @@ -27,10 +29,12 @@ constexpr int timeout_milliseconds = 30000; namespace multipass { -LXDMountHandler::LXDMountHandler(mp::NetworkAccessManager* network_manager, LXDVirtualMachine* lxd_virtual_machine, - const SSHKeyProvider* ssh_key_provider, const std::string& target_path, - const VMMount& mount) - : MountHandler{lxd_virtual_machine, ssh_key_provider, target_path, mount.source_path}, +LXDMountHandler::LXDMountHandler(mp::NetworkAccessManager* network_manager, + LXDVirtualMachine* lxd_virtual_machine, + const SSHKeyProvider* ssh_key_provider, + const std::string& target_path, + VMMount mount_spec) + : MountHandler{lxd_virtual_machine, ssh_key_provider, std::move(mount_spec), target_path}, network_manager{network_manager}, lxd_instance_endpoint{ QString("%1/instances/%2").arg(lxd_socket_url.toString(), lxd_virtual_machine->vm_name.c_str())}, diff --git a/src/platform/backends/lxd/lxd_mount_handler.h b/src/platform/backends/lxd/lxd_mount_handler.h index 713135ff914..fede0467956 100644 --- a/src/platform/backends/lxd/lxd_mount_handler.h +++ b/src/platform/backends/lxd/lxd_mount_handler.h @@ -26,8 +26,11 @@ namespace multipass class LXDMountHandler : public MountHandler { public: - LXDMountHandler(mp::NetworkAccessManager* network_manager, LXDVirtualMachine* lxd_virtual_machine, - const SSHKeyProvider* ssh_key_provider, const std::string& target_path, const VMMount& mount); + LXDMountHandler(NetworkAccessManager* network_manager, + LXDVirtualMachine* lxd_virtual_machine, + const SSHKeyProvider* ssh_key_provider, + const std::string& target_path, + VMMount mount_spec); ~LXDMountHandler() override; void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; @@ -43,7 +46,7 @@ class LXDMountHandler : public MountHandler void lxd_device_remove(); // data member - mp::NetworkAccessManager* network_manager{nullptr}; + NetworkAccessManager* network_manager{nullptr}; const QUrl lxd_instance_endpoint{}; const std::string device_name{}; }; diff --git a/src/platform/backends/lxd/lxd_virtual_machine.cpp b/src/platform/backends/lxd/lxd_virtual_machine.cpp index 80249b8f0cb..1d3d3f5142d 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine.cpp +++ b/src/platform/backends/lxd/lxd_virtual_machine.cpp @@ -166,10 +166,14 @@ bool uses_default_id_mappings(const multipass::VMMount& mount) } // namespace -mp::LXDVirtualMachine::LXDVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, - NetworkAccessManager* manager, const QUrl& base_url, - const QString& bridge_name, const QString& storage_pool) - : BaseVirtualMachine{desc.vm_name}, +mp::LXDVirtualMachine::LXDVirtualMachine(const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + NetworkAccessManager* manager, + const QUrl& base_url, + const QString& bridge_name, + const QString& storage_pool, + const mp::Path& instance_dir) + : BaseVirtualMachine{desc.vm_name, instance_dir}, name{QString::fromStdString(desc.vm_name)}, username{desc.ssh_username}, monitor{&monitor}, @@ -421,7 +425,7 @@ void mp::LXDVirtualMachine::request_state(const QString& new_state) } } -void multipass::LXDVirtualMachine::update_cpus(int num_cores) +void mp::LXDVirtualMachine::update_cpus(int num_cores) { assert(num_cores > 0); assert(manager); diff --git a/src/platform/backends/lxd/lxd_virtual_machine.h b/src/platform/backends/lxd/lxd_virtual_machine.h index 11569c62a96..5f224a88ce3 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine.h +++ b/src/platform/backends/lxd/lxd_virtual_machine.h @@ -18,7 +18,6 @@ #ifndef MULTIPASS_LXD_VIRTUAL_MACHINE_H #define MULTIPASS_LXD_VIRTUAL_MACHINE_H -#include #include #include @@ -33,8 +32,13 @@ class VMStatusMonitor; class LXDVirtualMachine : public BaseVirtualMachine { public: - LXDVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, NetworkAccessManager* manager, - const QUrl& base_url, const QString& bridge_name, const QString& storage_pool); + LXDVirtualMachine(const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + NetworkAccessManager* manager, + const QUrl& base_url, + const QString& bridge_name, + const QString& storage_pool, + const Path& instance_dir); ~LXDVirtualMachine() override; void stop() override; void start() override; diff --git a/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp b/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp index e70096cddd1..8c718cf5570 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp +++ b/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -81,13 +82,14 @@ mp::NetworkInterfaceInfo munch_network(std::map(desc, monitor, manager.get(), base_url, multipass_bridge_name, - storage_pool); + return std::make_unique(desc, + monitor, + manager.get(), + base_url, + multipass_bridge_name, + storage_pool, + MP_UTILS.make_dir(get_instance_directory(desc.vm_name))); } -void mp::LXDVirtualMachineFactory::remove_resources_for(const std::string& name) +void mp::LXDVirtualMachineFactory::remove_resources_for_impl(const std::string& name) { - mpl::log(mpl::Level::trace, category, fmt::format("No resources to remove for \"{}\"", name)); + mpl::log(mpl::Level::trace, category, fmt::format("No further resources to remove for \"{}\"", name)); } auto mp::LXDVirtualMachineFactory::prepare_source_image(const VMImage& source_image) -> VMImage @@ -205,7 +212,7 @@ void mp::LXDVirtualMachineFactory::hypervisor_health_check() } } -QString mp::LXDVirtualMachineFactory::get_backend_version_string() +QString mp::LXDVirtualMachineFactory::get_backend_version_string() const { auto reply = lxd_request(manager.get(), "GET", base_url); diff --git a/src/platform/backends/lxd/lxd_virtual_machine_factory.h b/src/platform/backends/lxd/lxd_virtual_machine_factory.h index f904633c8d6..e2259de9581 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine_factory.h +++ b/src/platform/backends/lxd/lxd_virtual_machine_factory.h @@ -37,15 +37,14 @@ class LXDVirtualMachineFactory : public BaseVirtualMachineFactory void prepare_networking(std::vector& extra_interfaces) override; VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor) override; - void remove_resources_for(const std::string& name) override; VMImage prepare_source_image(const VMImage& source_image) override; void prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) override; void hypervisor_health_check() override; - QString get_backend_directory_name() override + QString get_backend_directory_name() const override { return "lxd"; }; - QString get_backend_version_string() override; + QString get_backend_version_string() const override; VMImageVault::UPtr create_image_vault(std::vector image_hosts, URLDownloader* downloader, const Path& cache_dir_path, const Path& data_dir_path, const days& days_to_expire) override; @@ -54,11 +53,11 @@ class LXDVirtualMachineFactory : public BaseVirtualMachineFactory std::vector networks() const override; protected: + void remove_resources_for_impl(const std::string& name) override; std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; private: NetworkAccessManager::UPtr manager; - const Path data_dir; const QUrl base_url; QString storage_pool; }; diff --git a/src/platform/backends/lxd/lxd_vm_image_vault.cpp b/src/platform/backends/lxd/lxd_vm_image_vault.cpp index 7b3c8e56554..f39a57a765a 100644 --- a/src/platform/backends/lxd/lxd_vm_image_vault.cpp +++ b/src/platform/backends/lxd/lxd_vm_image_vault.cpp @@ -157,9 +157,13 @@ mp::LXDVMImageVault::LXDVMImageVault(std::vector image_hosts, URLD { } -mp::VMImage mp::LXDVMImageVault::fetch_image(const FetchType& fetch_type, const Query& query, - const PrepareAction& prepare, const ProgressMonitor& monitor, - const bool unlock, const std::optional& checksum) +mp::VMImage mp::LXDVMImageVault::fetch_image(const FetchType& fetch_type, + const Query& query, + const PrepareAction& prepare, + const ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const mp::Path& /* save_dir */) { // Look for an already existing instance and get its image info try diff --git a/src/platform/backends/lxd/lxd_vm_image_vault.h b/src/platform/backends/lxd/lxd_vm_image_vault.h index fe6ade82713..40f4f4e9f83 100644 --- a/src/platform/backends/lxd/lxd_vm_image_vault.h +++ b/src/platform/backends/lxd/lxd_vm_image_vault.h @@ -39,9 +39,13 @@ class LXDVMImageVault final : public BaseVMImageVault LXDVMImageVault(std::vector image_host, URLDownloader* downloader, NetworkAccessManager* manager, const QUrl& base_url, const QString& cache_dir_path, const multipass::days& days_to_expire); - VMImage fetch_image(const FetchType& fetch_type, const Query& query, const PrepareAction& prepare, - const ProgressMonitor& monitor, const bool unlock, - const std::optional& checksum) override; + VMImage fetch_image(const FetchType& fetch_type, + const Query& query, + const PrepareAction& prepare, + const ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const Path& /* save_dir */) override; void remove(const std::string& name) override; bool has_record_for(const std::string& name) override; void prune_expired_images() override; diff --git a/src/platform/backends/qemu/CMakeLists.txt b/src/platform/backends/qemu/CMakeLists.txt index 653c1d1e768..868ffa540af 100644 --- a/src/platform/backends/qemu/CMakeLists.txt +++ b/src/platform/backends/qemu/CMakeLists.txt @@ -25,6 +25,7 @@ add_definitions(-DHOST_ARCH="${HOST_ARCH}") add_library(qemu_backend STATIC qemu_base_process_spec.cpp qemu_mount_handler.cpp + qemu_snapshot.cpp qemu_vm_process_spec.cpp qemu_vmstate_process_spec.cpp qemu_virtual_machine_factory.cpp @@ -38,6 +39,7 @@ target_link_libraries(qemu_backend logger qemu_img_utils qemu_platform_detail + scope_guard utils Qt6::Core) diff --git a/src/platform/backends/qemu/qemu_mount_handler.cpp b/src/platform/backends/qemu/qemu_mount_handler.cpp index 63afa20d226..f40ee0fc4c3 100644 --- a/src/platform/backends/qemu/qemu_mount_handler.cpp +++ b/src/platform/backends/qemu/qemu_mount_handler.cpp @@ -21,6 +21,7 @@ #include +namespace mp = multipass; namespace mpl = multipass::logging; namespace mpu = multipass::utils; @@ -31,9 +32,11 @@ constexpr auto category = "qemu-mount-handler"; namespace multipass { -QemuMountHandler::QemuMountHandler(QemuVirtualMachine* vm, const SSHKeyProvider* ssh_key_provider, - const std::string& target, const VMMount& mount) - : MountHandler{vm, ssh_key_provider, target, mount.source_path}, +QemuMountHandler::QemuMountHandler(QemuVirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + const std::string& target, + VMMount mount_spec) + : MountHandler{vm, ssh_key_provider, std::move(mount_spec), target}, vm_mount_args{vm->modifiable_mount_args()}, // Create a reproducible unique mount tag for each mount. The cmd arg can only be 31 bytes long so part of the // uuid must be truncated. First character of tag must also be alphabetical. @@ -53,14 +56,16 @@ QemuMountHandler::QemuMountHandler(QemuVirtualMachine* vm, const SSHKeyProvider* } // Need to ensure no more than one uid/gid map is passed in here. - if (mount.uid_mappings.size() > 1 || mount.gid_mappings.size() > 1) + if (this->mount_spec.uid_mappings.size() > 1 || this->mount_spec.gid_mappings.size() > 1) throw std::runtime_error("Only one mapping per native mount allowed."); mpl::log(mpl::Level::info, category, fmt::format("initializing native mount {} => {} in '{}'", source, target, vm->vm_name)); - const auto uid_map = mount.uid_mappings.empty() ? std::make_pair(1000, 1000) : mount.uid_mappings[0]; - const auto gid_map = mount.gid_mappings.empty() ? std::make_pair(1000, 1000) : mount.gid_mappings[0]; + const auto uid_map = + this->mount_spec.uid_mappings.empty() ? std::make_pair(1000, 1000) : this->mount_spec.uid_mappings[0]; + const auto gid_map = + this->mount_spec.gid_mappings.empty() ? std::make_pair(1000, 1000) : this->mount_spec.gid_mappings[0]; const auto uid_arg = QString("uid_map=%1:%2,").arg(uid_map.first).arg(uid_map.second == -1 ? 1000 : uid_map.second); const auto gid_arg = QString{"gid_map=%1:%2,"}.arg(gid_map.first).arg(gid_map.second == -1 ? 1000 : gid_map.second); vm_mount_args[tag] = { diff --git a/src/platform/backends/qemu/qemu_mount_handler.h b/src/platform/backends/qemu/qemu_mount_handler.h index d263e997e2f..691aa657745 100644 --- a/src/platform/backends/qemu/qemu_mount_handler.h +++ b/src/platform/backends/qemu/qemu_mount_handler.h @@ -27,8 +27,10 @@ namespace multipass class QemuMountHandler : public MountHandler { public: - QemuMountHandler(QemuVirtualMachine* vm, const SSHKeyProvider* ssh_key_provider, const std::string& target, - const VMMount& mount); + QemuMountHandler(QemuVirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + const std::string& target, + VMMount mount_spec); ~QemuMountHandler() override; void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; diff --git a/src/platform/backends/qemu/qemu_platform.h b/src/platform/backends/qemu/qemu_platform.h index 953a161e722..ae8f37d049a 100644 --- a/src/platform/backends/qemu/qemu_platform.h +++ b/src/platform/backends/qemu/qemu_platform.h @@ -50,7 +50,7 @@ class QemuPlatform : private DisabledCopyMove return {}; }; virtual QStringList vm_platform_args(const VirtualMachineDescription& vm_desc) = 0; - virtual QString get_directory_name() + virtual QString get_directory_name() const { return {}; }; diff --git a/src/platform/backends/qemu/qemu_snapshot.cpp b/src/platform/backends/qemu/qemu_snapshot.cpp new file mode 100644 index 00000000000..cd02cc8decb --- /dev/null +++ b/src/platform/backends/qemu/qemu_snapshot.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "qemu_snapshot.h" +#include "qemu_virtual_machine.h" +#include "shared/qemu_img_utils/qemu_img_utils.h" + +#include +#include +#include +#include + +#include + +#include + +namespace mp = multipass; + +namespace +{ +std::unique_ptr make_capture_spec(const QString& tag, const mp::Path& image_path) +{ + return std::make_unique(QStringList{"snapshot", "-c", tag, image_path}, + /* src_img = */ "", + image_path); +} + +std::unique_ptr make_restore_spec(const QString& tag, const mp::Path& image_path) +{ + return std::make_unique(QStringList{"snapshot", "-a", tag, image_path}, + /* src_img = */ "", + image_path); +} + +std::unique_ptr make_delete_spec(const QString& tag, const mp::Path& image_path) +{ + return std::make_unique(QStringList{"snapshot", "-d", tag, image_path}, + /* src_img = */ "", + image_path); +} +} // namespace + +mp::QemuSnapshot::QemuSnapshot(const std::string& name, + const std::string& comment, + std::shared_ptr parent, + const VMSpecs& specs, + QemuVirtualMachine& vm, + VirtualMachineDescription& desc) + : BaseSnapshot{name, comment, std::move(parent), specs, vm}, desc{desc}, image_path{desc.image.image_path} +{ +} + +mp::QemuSnapshot::QemuSnapshot(const QString& filename, QemuVirtualMachine& vm, VirtualMachineDescription& desc) + : BaseSnapshot{filename, vm}, desc{desc}, image_path{desc.image.image_path} +{ +} + +void mp::QemuSnapshot::capture_impl() +{ + const auto& tag = get_id(); + + // Avoid creating more than one snapshot with the same tag (creation would succeed, but we'd then be unable to + // identify the snapshot by tag) + if (backend::instance_image_has_snapshot(image_path, tag)) + throw std::runtime_error{ + fmt::format("A snapshot with the same tag already exists in the image. Image: {}; tag: {})", + image_path, + tag)}; + + mp::backend::checked_exec_qemu_img(make_capture_spec(tag, image_path)); +} + +void mp::QemuSnapshot::erase_impl() +{ + const auto& tag = get_id(); + if (backend::instance_image_has_snapshot(image_path, tag)) + mp::backend::checked_exec_qemu_img(make_delete_spec(tag, image_path)); + else + mpl::log( + mpl::Level::warning, + BaseSnapshot::get_name(), + fmt::format("Could not find the underlying QEMU snapshot. Assuming it is already gone. Image: {}; tag: {}", + image_path, + tag)); +} + +void mp::QemuSnapshot::apply_impl() +{ + auto rollback = sg::make_scope_guard( + [this, old_desc = desc]() noexcept { top_catch_all(get_name(), [this, &old_desc]() { desc = old_desc; }); }); + + desc.num_cores = get_num_cores(); + desc.mem_size = get_mem_size(); + desc.disk_space = get_disk_space(); + + mp::backend::checked_exec_qemu_img(make_restore_spec(get_id(), image_path)); + rollback.dismiss(); +} diff --git a/src/platform/backends/qemu/qemu_snapshot.h b/src/platform/backends/qemu/qemu_snapshot.h new file mode 100644 index 00000000000..caff224631f --- /dev/null +++ b/src/platform/backends/qemu/qemu_snapshot.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_QEMU_SNAPSHOT_H +#define MULTIPASS_QEMU_SNAPSHOT_H + +#include "qemu_virtual_machine.h" + +#include + +#include + +namespace multipass +{ +class QemuVirtualMachine; +class VirtualMachineDescription; + +class QemuSnapshot : public BaseSnapshot +{ +public: + QemuSnapshot(const std::string& name, + const std::string& comment, + std::shared_ptr parent, + const VMSpecs& specs, + QemuVirtualMachine& vm, + VirtualMachineDescription& desc); + QemuSnapshot(const QString& filename, QemuVirtualMachine& vm, VirtualMachineDescription& desc); + +protected: + void capture_impl() override; + void erase_impl() override; + void apply_impl() override; + +private: + VirtualMachineDescription& desc; + const Path& image_path; +}; + +} // namespace multipass + +#endif // MULTIPASS_QEMU_SNAPSHOT_H diff --git a/src/platform/backends/qemu/qemu_virtual_machine.cpp b/src/platform/backends/qemu/qemu_virtual_machine.cpp index 35b5dd6a285..b34e94918d6 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine.cpp @@ -17,6 +17,7 @@ #include "qemu_virtual_machine.h" #include "qemu_mount_handler.h" +#include "qemu_snapshot.h" #include "qemu_vm_process_spec.h" #include "qemu_vmstate_process_spec.h" @@ -27,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -148,30 +148,6 @@ auto hmc_to_qmp_json(const QString& command_line) return QJsonDocument(qmp).toJson(); } -bool instance_image_has_snapshot(const mp::Path& image_path) -{ - auto process = - mp::platform::make_process(mp::simple_process_spec("qemu-img", QStringList{"snapshot", "-l", image_path})); - auto process_state = process->execute(); - if (!process_state.completed_successfully()) - { - throw std::runtime_error(fmt::format("Internal error: qemu-img failed ({}) with output:\n{}", - process_state.failure_message(), process->read_all_standard_error())); - } - - auto output = process->read_all_standard_output().split('\n'); - - for (const auto& line : output) - { - if (line.contains(suspend_tag)) - { - return true; - } - } - - return false; -} - auto get_qemu_machine_type(const QStringList& platform_args) { QTemporaryFile dump_file; @@ -220,10 +196,14 @@ auto generate_metadata(const QStringList& platform_args, const QStringList& proc } } // namespace -mp::QemuVirtualMachine::QemuVirtualMachine(const VirtualMachineDescription& desc, QemuPlatform* qemu_platform, - VMStatusMonitor& monitor) - : BaseVirtualMachine{instance_image_has_snapshot(desc.image.image_path) ? State::suspended : State::off, - desc.vm_name}, +mp::QemuVirtualMachine::QemuVirtualMachine(const VirtualMachineDescription& desc, + QemuPlatform* qemu_platform, + VMStatusMonitor& monitor, + const mp::Path& instance_dir) + : BaseVirtualMachine{mp::backend::instance_image_has_snapshot(desc.image.image_path, suspend_tag) ? State::suspended + : State::off, + desc.vm_name, + instance_dir}, desc{desc}, mac_addr{desc.default_mac_address}, username{desc.ssh_username}, @@ -231,6 +211,9 @@ mp::QemuVirtualMachine::QemuVirtualMachine(const VirtualMachineDescription& desc monitor{&monitor}, mount_args{mount_args_from_json(monitor.retrieve_metadata_for(vm_name))} { + // convert existing VMs to v3 too (doesn't affect images that are already v3) + mp::backend::amend_to_qcow2_v3(desc.image.image_path); // TODO drop in a couple of releases (going in on v1.13) + QObject::connect( this, &QemuVirtualMachine::on_delete_memory_snapshot, this, [this] { @@ -637,3 +620,17 @@ mp::QemuVirtualMachine::MountArgs& mp::QemuVirtualMachine::modifiable_mount_args { return mount_args; } + +auto mp::QemuVirtualMachine::make_specific_snapshot(const std::string& snapshot_name, + const std::string& comment, + const VMSpecs& specs, + std::shared_ptr parent) -> std::shared_ptr +{ + assert(state == VirtualMachine::State::off || state != VirtualMachine::State::stopped); // would need QMP otherwise + return std::make_shared(snapshot_name, comment, std::move(parent), specs, *this, desc); +} + +auto mp::QemuVirtualMachine::make_specific_snapshot(const QString& filename) -> std::shared_ptr +{ + return std::make_shared(filename, *this, desc); +} diff --git a/src/platform/backends/qemu/qemu_virtual_machine.h b/src/platform/backends/qemu/qemu_virtual_machine.h index f0bd31ce66f..ce0c73f01a8 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.h +++ b/src/platform/backends/qemu/qemu_virtual_machine.h @@ -41,7 +41,10 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine public: using MountArgs = std::unordered_map>; - QemuVirtualMachine(const VirtualMachineDescription& desc, QemuPlatform* qemu_platform, VMStatusMonitor& monitor); + QemuVirtualMachine(const VirtualMachineDescription& desc, + QemuPlatform* qemu_platform, + VMStatusMonitor& monitor, + const Path& instance_dir); ~QemuVirtualMachine(); void start() override; @@ -69,10 +72,17 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine void on_reset_network(); protected: - QemuVirtualMachine(const std::string& name) : BaseVirtualMachine{name} + // TODO remove this, the onus of composing a VM of stubs should be on the stub VMs + QemuVirtualMachine(const std::string& name, const Path& instance_dir) : BaseVirtualMachine{name, instance_dir} { } + std::shared_ptr make_specific_snapshot(const QString& filename) override; + std::shared_ptr make_specific_snapshot(const std::string& snapshot_name, + const std::string& comment, + const VMSpecs& specs, + std::shared_ptr parent) override; + private: void on_started(); void on_error(); diff --git a/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp b/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp index 8f5d9c2b937..9b554a8e25c 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp @@ -37,17 +37,27 @@ constexpr auto category = "qemu factory"; } // namespace mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(const mp::Path& data_dir) - : qemu_platform{MP_QEMU_PLATFORM_FACTORY.make_qemu_platform(data_dir)} + : QemuVirtualMachineFactory{MP_QEMU_PLATFORM_FACTORY.make_qemu_platform(data_dir), data_dir} +{ +} + +mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, const mp::Path& data_dir) + : BaseVirtualMachineFactory( + MP_UTILS.derive_instances_dir(data_dir, qemu_platform->get_directory_name(), instances_subdir)), + qemu_platform{std::move(qemu_platform)} { } mp::VirtualMachine::UPtr mp::QemuVirtualMachineFactory::create_virtual_machine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor) { - return std::make_unique(desc, qemu_platform.get(), monitor); + return std::make_unique(desc, + qemu_platform.get(), + monitor, + get_instance_directory(desc.vm_name)); } -void mp::QemuVirtualMachineFactory::remove_resources_for(const std::string& name) +void mp::QemuVirtualMachineFactory::remove_resources_for_impl(const std::string& name) { qemu_platform->remove_resources_for(name); } @@ -56,6 +66,7 @@ mp::VMImage mp::QemuVirtualMachineFactory::prepare_source_image(const mp::VMImag { VMImage image{source_image}; image.image_path = mp::backend::convert_to_qcow_if_necessary(source_image.image_path); + mp::backend::amend_to_qcow2_v3(image.image_path); return image; } @@ -70,7 +81,7 @@ void mp::QemuVirtualMachineFactory::hypervisor_health_check() qemu_platform->platform_health_check(); } -QString mp::QemuVirtualMachineFactory::get_backend_version_string() +QString mp::QemuVirtualMachineFactory::get_backend_version_string() const { auto process = mp::platform::make_process(simple_process_spec(QString("qemu-system-%1").arg(HOST_ARCH), {"--version"})); @@ -109,7 +120,7 @@ QString mp::QemuVirtualMachineFactory::get_backend_version_string() return QString("qemu-unknown"); } -QString mp::QemuVirtualMachineFactory::get_backend_directory_name() +QString mp::QemuVirtualMachineFactory::get_backend_directory_name() const { return qemu_platform->get_directory_name(); } diff --git a/src/platform/backends/qemu/qemu_virtual_machine_factory.h b/src/platform/backends/qemu/qemu_virtual_machine_factory.h index b0e6b918c2b..bc09eedf16c 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.h +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.h @@ -35,15 +35,19 @@ class QemuVirtualMachineFactory final : public BaseVirtualMachineFactory VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor) override; - void remove_resources_for(const std::string& name) override; VMImage prepare_source_image(const VMImage& source_image) override; void prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) override; void hypervisor_health_check() override; - QString get_backend_version_string() override; - QString get_backend_directory_name() override; + QString get_backend_version_string() const override; + QString get_backend_directory_name() const override; std::vector networks() const override; +protected: + void remove_resources_for_impl(const std::string& name) override; + private: + QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, const Path& data_dir); + QemuPlatform::UPtr qemu_platform; }; } // namespace multipass diff --git a/src/platform/backends/shared/CMakeLists.txt b/src/platform/backends/shared/CMakeLists.txt index e7fd55ee1d7..a9474b3fc0a 100644 --- a/src/platform/backends/shared/CMakeLists.txt +++ b/src/platform/backends/shared/CMakeLists.txt @@ -13,6 +13,7 @@ # along with this program. If not, see . add_library(shared STATIC + base_snapshot.cpp base_virtual_machine.cpp base_virtual_machine_factory.cpp sshfs_server_process_spec.cpp @@ -21,6 +22,7 @@ add_library(shared STATIC target_link_libraries(shared iso process + scope_guard utils Qt6::Core) diff --git a/src/platform/backends/shared/base_snapshot.cpp b/src/platform/backends/shared/base_snapshot.cpp new file mode 100644 index 00000000000..d613993b95a --- /dev/null +++ b/src/platform/backends/shared/base_snapshot.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "base_snapshot.h" +#include "multipass/virtual_machine.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +namespace mp = multipass; + +namespace +{ +constexpr auto snapshot_extension = "snapshot.json"; +constexpr auto index_digits = 4; // these two go together +constexpr auto max_snapshots = 9999; +const auto snapshot_template = QStringLiteral("@s%1"); /* avoid confusion with snapshot names by prepending a character + that can't be part of the name (users can call a snapshot + "s1", but they cannot call it "@s1") */ + +QString derive_index_string(int index) +{ + return QString{"%1"}.arg(index, index_digits, 10, QLatin1Char('0')); +} + +QJsonObject read_snapshot_json(const QString& filename) +{ + QFile file{filename}; + if (!MP_FILEOPS.open(file, QIODevice::ReadOnly)) + throw std::runtime_error{fmt::format("Could not open snapshot file for for reading: {}", file.fileName())}; + + QJsonParseError parse_error{}; + const auto& data = MP_FILEOPS.read_all(file); + + if (const auto json = QJsonDocument::fromJson(data, &parse_error).object(); parse_error.error) + throw std::runtime_error{fmt::format("Could not parse snapshot JSON; error: {}; file: {}", + file.fileName(), + parse_error.errorString())}; + else if (json.isEmpty()) + throw std::runtime_error{fmt::format("Empty snapshot JSON: {}", file.fileName())}; + else + return json["snapshot"].toObject(); +} + +std::unordered_map load_mounts(const QJsonArray& mounts_json) +{ + std::unordered_map mounts; + for (const auto& entry : mounts_json) + { + const auto& json = entry.toObject(); + mounts[json["target_path"].toString().toStdString()] = mp::VMMount{json}; + } + + return mounts; +} + +std::shared_ptr find_parent(const QJsonObject& json, mp::VirtualMachine& vm) +{ + auto parent_idx = json["parent"].toInt(); + try + { + return parent_idx ? vm.get_snapshot(parent_idx) : nullptr; + } + catch (std::out_of_range&) + { + throw std::runtime_error{fmt::format("Missing snapshot parent. Snapshot name: {}; parent index: {}", + json["name"].toString(), + parent_idx)}; + } +} +} // namespace + +mp::BaseSnapshot::BaseSnapshot(const std::string& name, // NOLINT(modernize-pass-by-value) + const std::string& comment, // NOLINT(modernize-pass-by-value) + std::shared_ptr parent, + int index, + QDateTime&& creation_timestamp, + int num_cores, + MemorySize mem_size, + MemorySize disk_space, + VirtualMachine::State state, + std::unordered_map mounts, + QJsonObject metadata, + const QDir& storage_dir, + bool captured) + : name{name}, + comment{comment}, + parent{std::move(parent)}, + index{index}, + id{snapshot_template.arg(index)}, + creation_timestamp{std::move(creation_timestamp)}, + num_cores{num_cores}, + mem_size{mem_size}, + disk_space{disk_space}, + state{state}, + mounts{std::move(mounts)}, + metadata{std::move(metadata)}, + storage_dir{storage_dir}, + captured{captured} +{ + assert(index > 0 && "snapshot indices need to start at 1"); + using St = VirtualMachine::State; + if (state != St::off && state != St::stopped) + throw std::runtime_error{fmt::format("Unsupported VM state in snapshot: {}", static_cast(state))}; + if (index < 1) + throw std::runtime_error{fmt::format("Snapshot index not positive: {}", index)}; + if (index > max_snapshots) + throw std::runtime_error{fmt::format("Maximum number of snapshots exceeded: {}", index)}; + if (name.empty()) + throw std::runtime_error{"Snapshot names cannot be empty"}; + if (num_cores < 1) + throw std::runtime_error{fmt::format("Invalid number of cores for snapshot: {}", num_cores)}; + if (auto mem_bytes = mem_size.in_bytes(); mem_bytes < 1) + throw std::runtime_error{fmt::format("Invalid memory size for snapshot: {}", mem_bytes)}; + if (auto disk_bytes = disk_space.in_bytes(); disk_bytes < 1) + throw std::runtime_error{fmt::format("Invalid disk size for snapshot: {}", disk_bytes)}; +} + +mp::BaseSnapshot::BaseSnapshot(const std::string& name, + const std::string& comment, + std::shared_ptr parent, + const VMSpecs& specs, + const VirtualMachine& vm) + : BaseSnapshot{name, + comment, + std::move(parent), + vm.get_snapshot_count() + 1, + QDateTime::currentDateTimeUtc(), + specs.num_cores, + specs.mem_size, + specs.disk_space, + specs.state, + specs.mounts, + specs.metadata, + vm.instance_directory(), + /*captured=*/false} +{ +} + +mp::BaseSnapshot::BaseSnapshot(const QString& filename, VirtualMachine& vm) + : BaseSnapshot{read_snapshot_json(filename), vm} +{ +} + +mp::BaseSnapshot::BaseSnapshot(const QJsonObject& json, VirtualMachine& vm) + : BaseSnapshot{ + json["name"].toString().toStdString(), // name + json["comment"].toString().toStdString(), // comment + find_parent(json, vm), // parent + json["index"].toInt(), // index + QDateTime::fromString(json["creation_timestamp"].toString(), Qt::ISODateWithMs), // creation_timestamp + json["num_cores"].toInt(), // num_cores + MemorySize{json["mem_size"].toString().toStdString()}, // mem_size + MemorySize{json["disk_space"].toString().toStdString()}, // disk_space + static_cast(json["state"].toInt()), // state + load_mounts(json["mounts"].toArray()), // mounts + json["metadata"].toObject(), // metadata + vm.instance_directory(), // storage_dir + true} // captured +{ +} + +QJsonObject mp::BaseSnapshot::serialize() const +{ + assert(captured && "precondition: only captured snapshots can be serialized"); + QJsonObject ret, snapshot{}; + const std::unique_lock lock{mutex}; + + snapshot.insert("name", QString::fromStdString(name)); + snapshot.insert("comment", QString::fromStdString(comment)); + snapshot.insert("parent", get_parents_index()); + snapshot.insert("index", index); + snapshot.insert("creation_timestamp", creation_timestamp.toString(Qt::ISODateWithMs)); + snapshot.insert("num_cores", num_cores); + snapshot.insert("mem_size", QString::number(mem_size.in_bytes())); + snapshot.insert("disk_space", QString::number(disk_space.in_bytes())); + snapshot.insert("state", static_cast(state)); + snapshot.insert("metadata", metadata); + + // Extract mount serialization + QJsonArray json_mounts; + for (const auto& mount : mounts) + { + auto entry = mount.second.serialize(); + entry.insert("target_path", QString::fromStdString(mount.first)); + json_mounts.append(entry); + } + + snapshot.insert("mounts", json_mounts); + ret.insert("snapshot", snapshot); + + return ret; +} + +void mp::BaseSnapshot::persist() const +{ + const std::unique_lock lock{mutex}; + + auto snapshot_filepath = storage_dir.filePath(derive_snapshot_filename()); + MP_JSONUTILS.write_json(serialize(), snapshot_filepath); +} + +auto mp::BaseSnapshot::erase_helper() +{ + // Remove snapshot file + auto tmp_dir = std::make_unique(); // work around no move ctor + if (!tmp_dir->isValid()) + throw std::runtime_error{"Could not create temporary directory"}; + + const auto snapshot_filename = derive_snapshot_filename(); + auto snapshot_filepath = storage_dir.filePath(snapshot_filename); + auto deleting_filepath = tmp_dir->filePath(snapshot_filename); + + QFile snapshot_file{snapshot_filepath}; + if (!MP_FILEOPS.rename(snapshot_file, deleting_filepath)) + throw std::runtime_error{ + fmt::format("Failed to move snapshot file to temporary destination: {}", deleting_filepath)}; + + return sg::make_scope_guard([tmp_dir = std::move(tmp_dir), + deleting_filepath = std::move(deleting_filepath), + snapshot_filepath = std::move(snapshot_filepath)]() noexcept { + QFile temp_file{deleting_filepath}; + MP_FILEOPS.rename(temp_file, snapshot_filepath); // best effort, ignore return + }); +} + +void mp::BaseSnapshot::erase() +{ + const std::unique_lock lock{mutex}; + assert(captured && "precondition: only captured snapshots can be erased"); + + auto rollback_snapshot_file = erase_helper(); + erase_impl(); + rollback_snapshot_file.dismiss(); +} + +QString mp::BaseSnapshot::derive_snapshot_filename() const +{ + return QString{"%1.%2"}.arg(derive_index_string(index), snapshot_extension); +} diff --git a/src/platform/backends/shared/base_snapshot.h b/src/platform/backends/shared/base_snapshot.h new file mode 100644 index 00000000000..eda4fd6fdbb --- /dev/null +++ b/src/platform/backends/shared/base_snapshot.h @@ -0,0 +1,253 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_BASE_SNAPSHOT_H +#define MULTIPASS_BASE_SNAPSHOT_H + +#include + +#include +#include + +#include +#include + +#include + +namespace multipass +{ +struct VMSpecs; + +class BaseSnapshot : public Snapshot +{ +public: + BaseSnapshot(const std::string& name, + const std::string& comment, + std::shared_ptr parent, + const VMSpecs& specs, + const VirtualMachine& vm); + BaseSnapshot(const QString& filename, VirtualMachine& vm); + + int get_index() const noexcept override; + std::string get_name() const override; + std::string get_comment() const override; + QDateTime get_creation_timestamp() const noexcept override; + int get_num_cores() const noexcept override; + MemorySize get_mem_size() const noexcept override; + MemorySize get_disk_space() const noexcept override; + VirtualMachine::State get_state() const noexcept override; + + // Note that these return references - careful not to delete the snapshot while they are in use + const std::unordered_map& get_mounts() const noexcept override; + const QJsonObject& get_metadata() const noexcept override; + + std::shared_ptr get_parent() const override; + std::shared_ptr get_parent() override; + std::string get_parents_name() const override; + int get_parents_index() const override; + + void set_name(const std::string& n) override; + void set_comment(const std::string& c) override; + void set_parent(std::shared_ptr p) override; + + void capture() final; + void erase() final; + void apply() final; + +protected: + const QString& get_id() const noexcept; + + virtual void capture_impl() = 0; + virtual void erase_impl() = 0; + virtual void apply_impl() = 0; + +private: + BaseSnapshot(const QJsonObject& json, VirtualMachine& vm); + BaseSnapshot(const std::string& name, + const std::string& comment, + std::shared_ptr parent, + int index, + QDateTime&& creation_timestamp, + int num_cores, + MemorySize mem_size, + MemorySize disk_space, + VirtualMachine::State state, + std::unordered_map mounts, + QJsonObject metadata, + const QDir& storage_dir, + bool captured); + + auto erase_helper(); + QString derive_snapshot_filename() const; + QJsonObject serialize() const; + void persist() const; + +private: + std::string name; + std::string comment; + std::shared_ptr parent; + + // This class is non-copyable and having these const simplifies thread safety + const int index; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const QString id; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const QDateTime creation_timestamp; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const int num_cores; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const MemorySize mem_size; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const MemorySize disk_space; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const VirtualMachine::State state; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const std::unordered_map mounts; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const QJsonObject metadata; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const QDir storage_dir; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + + bool captured; + mutable std::recursive_mutex mutex; +}; +} // namespace multipass + +inline std::string multipass::BaseSnapshot::get_name() const +{ + const std::unique_lock lock{mutex}; + return name; +} + +inline std::string multipass::BaseSnapshot::get_comment() const +{ + const std::unique_lock lock{mutex}; + return comment; +} + +inline int multipass::BaseSnapshot::get_index() const noexcept +{ + return index; +} + +inline QDateTime multipass::BaseSnapshot::get_creation_timestamp() const noexcept +{ + return creation_timestamp; +} + +inline std::string multipass::BaseSnapshot::get_parents_name() const +{ + std::unique_lock lock{mutex}; + auto par = parent; + lock.unlock(); // avoid locking another snapshot while locked in here + + return par ? par->get_name() : ""; +} + +inline int multipass::BaseSnapshot::get_parents_index() const +{ + const std::unique_lock lock{mutex}; + return parent ? parent->get_index() : 0; // this doesn't lock +} + +inline auto multipass::BaseSnapshot::get_parent() const -> std::shared_ptr +{ + const std::unique_lock lock{mutex}; + return parent; +} + +inline auto multipass::BaseSnapshot::get_parent() -> std::shared_ptr +{ + return std::const_pointer_cast(std::as_const(*this).get_parent()); +} + +inline int multipass::BaseSnapshot::get_num_cores() const noexcept +{ + return num_cores; +} + +inline auto multipass::BaseSnapshot::get_mem_size() const noexcept -> MemorySize +{ + return mem_size; +} + +inline auto multipass::BaseSnapshot::get_disk_space() const noexcept -> MemorySize +{ + return disk_space; +} + +inline auto multipass::BaseSnapshot::get_state() const noexcept -> VirtualMachine::State +{ + return state; +} + +inline auto multipass::BaseSnapshot::get_mounts() const noexcept -> const std::unordered_map& +{ + return mounts; +} + +inline const QJsonObject& multipass::BaseSnapshot::get_metadata() const noexcept +{ + return metadata; +} + +inline void multipass::BaseSnapshot::set_name(const std::string& n) +{ + const std::unique_lock lock{mutex}; + assert(captured && "precondition: only captured snapshots can be edited"); + + name = n; + persist(); +} + +inline void multipass::BaseSnapshot::set_comment(const std::string& c) +{ + const std::unique_lock lock{mutex}; + assert(captured && "precondition: only captured snapshots can be edited"); + + comment = c; + persist(); +} + +inline void multipass::BaseSnapshot::set_parent(std::shared_ptr p) +{ + const std::unique_lock lock{mutex}; + assert(captured && "precondition: only captured snapshots can be edited"); + + parent = std::move(p); + persist(); +} + +inline void multipass::BaseSnapshot::capture() +{ + const std::unique_lock lock{mutex}; + assert(!captured && + "pre-condition: capture should only be called once, and only for snapshots that were not loaded from disk"); + + if (!captured) + { + captured = true; + capture_impl(); + persist(); + } +} + +inline void multipass::BaseSnapshot::apply() +{ + const std::unique_lock lock{mutex}; + apply_impl(); + // no need to persist here for the time being: only private fields of the base class are persisted for now, and + // those cannot be affected by apply_impl (except by setters, which already persist) +} + +inline const QString& multipass::BaseSnapshot::get_id() const noexcept +{ + return id; +} + +#endif // MULTIPASS_BASE_SNAPSHOT_H diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index 02aaddb9f39..6ac33af7eba 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -17,20 +17,71 @@ #include "base_virtual_machine.h" +#include +#include #include +#include #include +#include +#include +#include +#include + +#include + +#include namespace mp = multipass; namespace mpl = multipass::logging; +namespace mpu = multipass::utils; + +namespace +{ +using St = mp::VirtualMachine::State; +constexpr auto snapshot_extension = "snapshot.json"; +constexpr auto head_filename = "snapshot-head"; +constexpr auto count_filename = "snapshot-count"; +constexpr auto yes_overwrite = true; + +void assert_vm_stopped(St state) +{ + assert(state == St::off || state == St::stopped); +} + +mp::Path derive_head_path(const QDir& snapshot_dir) +{ + return snapshot_dir.filePath(head_filename); +} + +void update_parents_rollback_helper(const std::shared_ptr& deleted_parent, + std::vector& updated_parents) +{ + for (auto snapshot : updated_parents) + snapshot->set_parent(deleted_parent); +} + +std::string trimmed_contents_of(const QString& file_path) +{ + return mpu::trim(mpu::contents_of(file_path)); +} +} // namespace namespace multipass { +BaseVirtualMachine::BaseVirtualMachine(VirtualMachine::State state, + const std::string& vm_name, + const mp::Path& instance_dir) + : VirtualMachine(state, vm_name, instance_dir){}; + +BaseVirtualMachine::BaseVirtualMachine(const std::string& vm_name, const mp::Path& instance_dir) + : VirtualMachine(vm_name, instance_dir){}; + std::vector BaseVirtualMachine::get_all_ipv4(const SSHKeyProvider& key_provider) { std::vector all_ipv4; - if (current_state() == State::running) + if (current_state() == St::running) { QString ip_a_output; @@ -56,11 +107,448 @@ std::vector BaseVirtualMachine::get_all_ipv4(const SSHKeyProvider& } catch (const SSHException& e) { - mpl::log(mpl::Level::debug, "base_vm", fmt::format("Error getting extra IP addresses: {}", e.what())); + mpl::log(mpl::Level::debug, vm_name, fmt::format("Error getting extra IP addresses: {}", e.what())); } } return all_ipv4; } +auto BaseVirtualMachine::view_snapshots() const -> SnapshotVista +{ + SnapshotVista ret; + + const std::unique_lock lock{snapshot_mutex}; + ret.reserve(snapshots.size()); + std::transform(std::cbegin(snapshots), std::cend(snapshots), std::back_inserter(ret), [](const auto& pair) { + return pair.second; + }); + + return ret; +} + +std::shared_ptr BaseVirtualMachine::get_snapshot(const std::string& name) const +{ + const std::unique_lock lock{snapshot_mutex}; + try + { + return snapshots.at(name); + } + catch (const std::out_of_range&) + { + throw NoSuchSnapshotException{vm_name, name}; + } +} + +std::shared_ptr BaseVirtualMachine::get_snapshot(int index) const +{ + const std::unique_lock lock{snapshot_mutex}; + + auto index_matcher = [index](const auto& elem) { return elem.second->get_index() == index; }; + if (auto it = std::find_if(snapshots.begin(), snapshots.end(), index_matcher); it != snapshots.end()) + return it->second; + + throw std::runtime_error{ + fmt::format("No snapshot with given index in instance; instance name: {}; snapshot index: {}", vm_name, index)}; +} + +std::shared_ptr BaseVirtualMachine::get_snapshot(const std::string& name) +{ + return std::const_pointer_cast(std::as_const(*this).get_snapshot(name)); +} + +std::shared_ptr BaseVirtualMachine::get_snapshot(int index) +{ + return std::const_pointer_cast(std::as_const(*this).get_snapshot(index)); +} + +void BaseVirtualMachine::take_snapshot_rollback_helper(SnapshotMap::iterator it, + std::shared_ptr& old_head, + int old_count) +{ + if (old_head != head_snapshot) + { + assert(it->second && "snapshot not created despite modified head"); + if (snapshot_count > old_count) // snapshot was captured + { + assert(snapshot_count - old_count == 1); + --snapshot_count; + + mp::top_catch_all(vm_name, [it] { it->second->erase(); }); + } + + head_snapshot = std::move(old_head); + } + + snapshots.erase(it); +} + +auto BaseVirtualMachine::make_take_snapshot_rollback(SnapshotMap::iterator it) +{ + return sg::make_scope_guard( // best effort to rollback + [this, it = it, old_head = head_snapshot, old_count = snapshot_count]() mutable noexcept { + take_snapshot_rollback_helper(it, old_head, old_count); + }); +} + +std::shared_ptr BaseVirtualMachine::take_snapshot(const VMSpecs& specs, + const std::string& snapshot_name, + const std::string& comment) +{ + std::unique_lock lock{snapshot_mutex}; + assert_vm_stopped(state); // precondition + + auto sname = snapshot_name.empty() ? generate_snapshot_name() : snapshot_name; + + const auto [it, success] = snapshots.try_emplace(sname, nullptr); + if (!success) + { + mpl::log(mpl::Level::warning, vm_name, fmt::format("Snapshot name taken: {}", sname)); + throw SnapshotNameTakenException{vm_name, sname}; + } + + auto rollback_on_failure = make_take_snapshot_rollback(it); + + auto ret = head_snapshot = it->second = make_specific_snapshot(sname, comment, specs, head_snapshot); + ret->capture(); + + ++snapshot_count; + persist_generic_snapshot_info(); + + rollback_on_failure.dismiss(); + log_latest_snapshot(std::move(lock)); + + return ret; +} + +bool BaseVirtualMachine::updated_deleted_head(std::shared_ptr& snapshot, const Path& head_path) +{ + if (head_snapshot == snapshot) + { + head_snapshot = snapshot->get_parent(); + persist_head_snapshot_index(head_path); + return true; + } + + return false; +} + +auto BaseVirtualMachine::make_deleted_head_rollback(const Path& head_path, const bool& wrote_head) +{ + return sg::make_scope_guard([this, old_head = head_snapshot, &head_path, &wrote_head]() mutable noexcept { + deleted_head_rollback_helper(head_path, wrote_head, old_head); + }); +} + +void BaseVirtualMachine::deleted_head_rollback_helper(const Path& head_path, + const bool& wrote_head, + std::shared_ptr& old_head) +{ + if (head_snapshot != old_head) + { + head_snapshot = std::move(old_head); + if (wrote_head) + top_catch_all(vm_name, [this, &head_path] { + MP_UTILS.make_file_with_content(head_path.toStdString(), + std::to_string(head_snapshot->get_index()) + "\n", + yes_overwrite); + }); + } +} + +auto BaseVirtualMachine::make_parent_update_rollback(const std::shared_ptr& deleted_parent, + std::vector& updated_parents) const +{ + return sg::make_scope_guard([this, &updated_parents, deleted_parent]() noexcept { + top_catch_all(vm_name, &update_parents_rollback_helper, deleted_parent, updated_parents); + }); +} + +void BaseVirtualMachine::delete_snapshot_helper(std::shared_ptr& snapshot) +{ + // Update head if deleted + auto wrote_head = false; + auto head_path = derive_head_path(instance_dir); + auto rollback_head = make_deleted_head_rollback(head_path, wrote_head); + wrote_head = updated_deleted_head(snapshot, head_path); + + // Update children of deleted snapshot + std::vector updated_parents{}; + updated_parents.reserve(snapshots.size()); + + auto rollback_parent_updates = make_parent_update_rollback(snapshot, updated_parents); + update_parents(snapshot, updated_parents); + + // Erase the snapshot with the backend and dismiss rollbacks on success + snapshot->erase(); + rollback_parent_updates.dismiss(); + rollback_head.dismiss(); +} + +void BaseVirtualMachine::update_parents(std::shared_ptr& deleted_parent, + std::vector& updated_parents) +{ + auto new_parent = deleted_parent->get_parent(); + for (auto& [ignore, other] : snapshots) + { + if (other->get_parent() == deleted_parent) + { + other->set_parent(new_parent); + updated_parents.push_back(other.get()); + } + } +} + +template +auto BaseVirtualMachine::make_reinsert_guard(NodeT& snapshot_node) +{ + return sg::make_scope_guard([this, &snapshot_node]() noexcept { + top_catch_all(vm_name, [this, &snapshot_node] { + const auto& current_name = snapshot_node.mapped()->get_name(); + if (auto& key = snapshot_node.key(); key != current_name) + key = current_name; // best-effort rollback (this is very unlikely to fail) + + snapshots.insert(std::move(snapshot_node)); + }); + }); + ; +} + +void BaseVirtualMachine::rename_snapshot(const std::string& old_name, const std::string& new_name) +{ + if (old_name == new_name) + return; + + const std::unique_lock lock{snapshot_mutex}; + + auto old_it = snapshots.find(old_name); + if (old_it == snapshots.end()) + throw NoSuchSnapshotException{vm_name, old_name}; + + if (snapshots.find(new_name) != snapshots.end()) + throw SnapshotNameTakenException{vm_name, new_name}; + + auto snapshot_node = snapshots.extract(old_it); + const auto reinsert_guard = make_reinsert_guard(snapshot_node); // we want this to execute both on failure & success + + snapshot_node.key() = new_name; + snapshot_node.mapped()->set_name(new_name); +} + +void BaseVirtualMachine::delete_snapshot(const std::string& name) +{ + const std::unique_lock lock{snapshot_mutex}; + + auto it = snapshots.find(name); + if (it == snapshots.end()) + throw NoSuchSnapshotException{vm_name, name}; + + auto snapshot = it->second; + delete_snapshot_helper(snapshot); + + snapshots.erase(it); // doesn't throw + mpl::log(mpl::Level::debug, vm_name, fmt::format("Snapshot deleted: {}", name)); +} + +void BaseVirtualMachine::load_snapshots() +{ + const std::unique_lock lock{snapshot_mutex}; + + auto snapshot_files = MP_FILEOPS.entryInfoList(instance_dir, + {QString{"*.%1"}.arg(snapshot_extension)}, + QDir::Filter::Files | QDir::Filter::Readable, + QDir::SortFlag::Name); + for (const auto& finfo : snapshot_files) + load_snapshot(finfo.filePath()); + + load_generic_snapshot_info(); +} + +std::vector BaseVirtualMachine::get_childrens_names(const Snapshot* parent) const +{ + std::vector children; + + for (const auto& snapshot : view_snapshots()) + if (snapshot->get_parent().get() == parent) + children.push_back(snapshot->get_name()); + + return children; +} + +void BaseVirtualMachine::load_generic_snapshot_info() +{ + try + { + snapshot_count = std::stoi(trimmed_contents_of(instance_dir.filePath(count_filename))); + + auto head_index = std::stoi(trimmed_contents_of(instance_dir.filePath(head_filename))); + head_snapshot = head_index ? get_snapshot(head_index) : nullptr; + } + catch (FileOpenFailedException&) + { + if (!snapshots.empty()) + throw; + } +} + +template +void BaseVirtualMachine::log_latest_snapshot(LockT lock) const +{ + auto num_snapshots = static_cast(snapshots.size()); + auto parent_name = head_snapshot->get_parents_name(); + + assert(num_snapshots <= snapshot_count && "can't have more snapshots than were ever taken"); + + if (auto log_detail_lvl = mpl::Level::debug; log_detail_lvl <= mpl::get_logging_level()) + { + auto name = head_snapshot->get_name(); + lock.unlock(); // unlock earlier + + mpl::log(log_detail_lvl, + vm_name, + fmt::format(R"(New snapshot: "{}"; Descendant of: "{}"; Total snapshots: {})", + name, + parent_name, + num_snapshots)); + } +} + +void BaseVirtualMachine::load_snapshot(const QString& filename) +{ + auto snapshot = make_specific_snapshot(filename); + const auto& name = snapshot->get_name(); + auto [it, success] = snapshots.try_emplace(name, snapshot); + + if (!success) + { + mpl::log(mpl::Level::warning, vm_name, fmt::format("Snapshot name taken: {}", name)); + throw SnapshotNameTakenException{vm_name, name}; + } +} + +auto BaseVirtualMachine::make_common_file_rollback(const Path& file_path, + QFile& file, + const std::string& old_contents) const +{ + return sg::make_scope_guard([this, &file_path, &file, old_contents, existed = file.exists()]() noexcept { + common_file_rollback_helper(file_path, file, old_contents, existed); + }); +} + +void BaseVirtualMachine::common_file_rollback_helper(const Path& file_path, + QFile& file, + const std::string& old_contents, + bool existed) const +{ + // best effort, ignore returns + if (!existed) + file.remove(); + else + top_catch_all(vm_name, [&file_path, &old_contents] { + MP_UTILS.make_file_with_content(file_path.toStdString(), old_contents, yes_overwrite); + }); +} + +void BaseVirtualMachine::persist_generic_snapshot_info() const +{ + assert(head_snapshot); + + auto head_path = derive_head_path(instance_dir); + auto count_path = instance_dir.filePath(count_filename); + + QFile head_file{head_path}; + auto head_file_rollback = + make_common_file_rollback(head_path, head_file, std::to_string(head_snapshot->get_parents_index()) + "\n"); + persist_head_snapshot_index(head_path); + + QFile count_file{count_path}; + auto count_file_rollback = + make_common_file_rollback(count_path, count_file, std::to_string(snapshot_count - 1) + "\n"); + MP_UTILS.make_file_with_content(count_path.toStdString(), std::to_string(snapshot_count) + "\n", yes_overwrite); + + count_file_rollback.dismiss(); + head_file_rollback.dismiss(); +} + +void BaseVirtualMachine::persist_head_snapshot_index(const Path& head_path) const +{ + auto head_index = head_snapshot ? head_snapshot->get_index() : 0; + MP_UTILS.make_file_with_content(head_path.toStdString(), std::to_string(head_index) + "\n", yes_overwrite); +} + +std::string BaseVirtualMachine::generate_snapshot_name() const +{ + return fmt::format("snapshot{}", snapshot_count + 1); +} + +auto BaseVirtualMachine::make_restore_rollback(const Path& head_path, VMSpecs& specs) +{ + return sg::make_scope_guard([this, &head_path, old_head = head_snapshot, old_specs = specs, &specs]() noexcept { + top_catch_all(vm_name, + &BaseVirtualMachine::restore_rollback_helper, + this, + head_path, + old_head, + old_specs, + specs); + }); +} + +void BaseVirtualMachine::restore_rollback_helper(const Path& head_path, + const std::shared_ptr& old_head, + const VMSpecs& old_specs, + VMSpecs& specs) +{ + // best effort only + old_head->apply(); + specs = old_specs; + if (old_head != head_snapshot) + { + head_snapshot = old_head; + persist_head_snapshot_index(head_path); + } +} + +void BaseVirtualMachine::restore_snapshot(const std::string& name, VMSpecs& specs) +{ + const std::unique_lock lock{snapshot_mutex}; + assert_vm_stopped(state); // precondition + + auto snapshot = get_snapshot(name); + assert(snapshot->get_state() == St::off || snapshot->get_state() == St::stopped); + + snapshot->apply(); + + const auto head_path = derive_head_path(instance_dir); + auto rollback = make_restore_rollback(head_path, specs); + + specs.state = snapshot->get_state(); + specs.num_cores = snapshot->get_num_cores(); + specs.mem_size = snapshot->get_mem_size(); + specs.disk_space = snapshot->get_disk_space(); + specs.mounts = snapshot->get_mounts(); + specs.metadata = snapshot->get_metadata(); + + if (head_snapshot != snapshot) + { + head_snapshot = snapshot; + persist_head_snapshot_index(head_path); + } + + rollback.dismiss(); +} + +std::shared_ptr BaseVirtualMachine::make_specific_snapshot(const std::string& /*snapshot_name*/, + const std::string& /*comment*/, + const VMSpecs& /*specs*/, + std::shared_ptr /*parent*/) +{ + throw NotImplementedOnThisBackendException{"Snapshots"}; +} + +std::shared_ptr BaseVirtualMachine::make_specific_snapshot(const QString& /*filename*/) +{ + throw NotImplementedOnThisBackendException{"Snapshots"}; +} + } // namespace multipass diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 4ee04174986..8649a0bfcbe 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -29,26 +30,114 @@ #include #include -namespace mp = multipass; -namespace mpl = multipass::logging; -namespace mpu = multipass::utils; +#include +#include +#include namespace multipass { class BaseVirtualMachine : public VirtualMachine { public: - BaseVirtualMachine(VirtualMachine::State state, const std::string& vm_name) : VirtualMachine(state, vm_name){}; - BaseVirtualMachine(const std::string& vm_name) : VirtualMachine(vm_name){}; + BaseVirtualMachine(VirtualMachine::State state, const std::string& vm_name, const Path& instance_dir); + BaseVirtualMachine(const std::string& vm_name, const Path& instance_dir); std::vector get_all_ipv4(const SSHKeyProvider& key_provider) override; std::unique_ptr make_native_mount_handler(const SSHKeyProvider* ssh_key_provider, const std::string& target, - const multipass::VMMount& mount) override + const VMMount& mount) override { throw NotImplementedOnThisBackendException("native mounts"); }; + + SnapshotVista view_snapshots() const override; + int get_num_snapshots() const noexcept override; + + std::shared_ptr get_snapshot(const std::string& name) const override; + std::shared_ptr get_snapshot(int index) const override; + std::shared_ptr get_snapshot(const std::string& name) override; + std::shared_ptr get_snapshot(int index) override; + + // TODO: the VM should know its directory, but that is true of everything in its VMDescription; pulling that from + // derived classes is a big refactor + std::shared_ptr take_snapshot(const VMSpecs& specs, + const std::string& snapshot_name, + const std::string& comment) override; + void rename_snapshot(const std::string& old_name, const std::string& new_name) override; + void delete_snapshot(const std::string& name) override; + void restore_snapshot(const std::string& name, VMSpecs& specs) override; + void load_snapshots() override; + std::vector get_childrens_names(const Snapshot* parent) const override; + int get_snapshot_count() const override; + +protected: + virtual std::shared_ptr make_specific_snapshot(const QString& filename); + virtual std::shared_ptr make_specific_snapshot(const std::string& snapshot_name, + const std::string& comment, + const VMSpecs& specs, + std::shared_ptr parent); + +private: + using SnapshotMap = std::unordered_map>; + + template + void log_latest_snapshot(LockT lock) const; + + void load_generic_snapshot_info(); + void load_snapshot(const QString& filename); + + auto make_take_snapshot_rollback(SnapshotMap::iterator it); + void take_snapshot_rollback_helper(SnapshotMap::iterator it, std::shared_ptr& old_head, int old_count); + + auto make_common_file_rollback(const Path& file_path, QFile& file, const std::string& old_contents) const; + void common_file_rollback_helper(const Path& file_path, + QFile& file, + const std::string& old_contents, + bool existed) const; + + void persist_generic_snapshot_info() const; + void persist_head_snapshot_index(const Path& head_path) const; + std::string generate_snapshot_name() const; + + template + auto make_reinsert_guard(NodeT& snapshot_node); + + auto make_restore_rollback(const Path& head_path, VMSpecs& specs); + void restore_rollback_helper(const Path& head_path, + const std::shared_ptr& old_head, + const VMSpecs& old_specs, + VMSpecs& specs); + + bool updated_deleted_head(std::shared_ptr& snapshot, const Path& head_path); + auto make_deleted_head_rollback(const Path& head_path, const bool& wrote_head); + void deleted_head_rollback_helper(const Path& head_path, + const bool& wrote_head, + std::shared_ptr& old_head); + + void update_parents(std::shared_ptr& deleted_parent, std::vector& updated_parents); + auto make_parent_update_rollback(const std::shared_ptr& deleted_parent, + std::vector& updated_snapshot_paths) const; + + void delete_snapshot_helper(std::shared_ptr& snapshot); + +private: + SnapshotMap snapshots; + std::shared_ptr head_snapshot = nullptr; + int snapshot_count = 0; // tracks the number of snapshots ever taken (regardless or deletes) + mutable std::recursive_mutex snapshot_mutex; }; + } // namespace multipass +inline int multipass::BaseVirtualMachine::get_num_snapshots() const noexcept +{ + return static_cast(snapshots.size()); +} + +inline int multipass::BaseVirtualMachine::get_snapshot_count() const +{ + const std::unique_lock lock{snapshot_mutex}; + return snapshot_count; +} + #endif // MULTIPASS_BASE_VIRTUAL_MACHINE_H diff --git a/src/platform/backends/shared/base_virtual_machine_factory.cpp b/src/platform/backends/shared/base_virtual_machine_factory.cpp index 858b8e2293b..b7241c2f8ac 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.cpp +++ b/src/platform/backends/shared/base_virtual_machine_factory.cpp @@ -39,6 +39,10 @@ auto find_bridge_with(const NetworkContainer& networks, const std::string& membe } } // namespace +const mp::Path mp::BaseVirtualMachineFactory::instances_subdir = "vault/instances"; + +mp::BaseVirtualMachineFactory::BaseVirtualMachineFactory(const Path& instances_dir) : instances_dir{instances_dir} {}; + void mp::BaseVirtualMachineFactory::configure(VirtualMachineDescription& vm_desc) { auto instance_dir{mpu::base_dir(vm_desc.image.image_path)}; diff --git a/src/platform/backends/shared/base_virtual_machine_factory.h b/src/platform/backends/shared/base_virtual_machine_factory.h index 0355b412e10..7dff2907797 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.h +++ b/src/platform/backends/shared/base_virtual_machine_factory.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -33,18 +34,25 @@ constexpr auto log_category = "base factory"; class BaseVirtualMachineFactory : public VirtualMachineFactory { public: - BaseVirtualMachineFactory() = default; + explicit BaseVirtualMachineFactory(const Path& instances_dir); + + void remove_resources_for(const std::string& name) final; FetchType fetch_type() override { return FetchType::ImageOnly; }; - QString get_backend_directory_name() override + QString get_backend_directory_name() const override { return {}; }; + Path get_instance_directory(const std::string& name) const override + { + return utils::backend_directory_path(instances_dir, QString::fromStdString(name)); + } + void prepare_networking(std::vector& /*extra_interfaces*/) override { // only certain backends need to do anything to prepare networking @@ -65,6 +73,9 @@ class BaseVirtualMachineFactory : public VirtualMachineFactory throw NotImplementedOnThisBackendException("networks"); }; +protected: + static const Path instances_subdir; + protected: std::string create_bridge_with(const NetworkInterfaceInfo& interface) override { @@ -76,7 +87,19 @@ class BaseVirtualMachineFactory : public VirtualMachineFactory virtual void prepare_interface(NetworkInterface& net, std::vector& host_nets, const std::string& bridge_type); + + virtual void remove_resources_for_impl(const std::string& name) = 0; + +private: + Path instances_dir; }; } // namespace multipass +inline void multipass::BaseVirtualMachineFactory::remove_resources_for(const std::string& name) +{ + remove_resources_for_impl(name); + QDir instance_dir{get_instance_directory(name)}; + instance_dir.removeRecursively(); +} + #endif // MULTIPASS_BASE_VIRTUAL_MACHINE_FACTORY_H diff --git a/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.cpp b/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.cpp index b9d53df50c8..9bcc1b06dd6 100644 --- a/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.cpp +++ b/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.cpp @@ -25,25 +25,39 @@ #include #include +#include #include #include namespace mp = multipass; +namespace mpp = multipass::platform; -void mp::backend::resize_instance_image(const MemorySize& disk_space, const mp::Path& image_path) +auto mp::backend::checked_exec_qemu_img(std::unique_ptr spec, + const std::string& custom_error_prefix, + std::optional timeout) -> Process::UPtr { - auto disk_size = QString::number(disk_space.in_bytes()); // format documented in `man qemu-img` (look for "size") - QStringList qemuimg_parameters{{"resize", image_path, disk_size}}; - auto qemuimg_process = - mp::platform::make_process(std::make_unique(qemuimg_parameters, "", image_path)); + auto process = mpp::make_process(std::move(spec)); - auto process_state = qemuimg_process->execute(mp::image_resize_timeout); + auto process_state = timeout ? process->execute(*timeout) : process->execute(); if (!process_state.completed_successfully()) { - throw std::runtime_error(fmt::format("Cannot resize instance image: qemu-img failed ({}) with output:\n{}", + throw std::runtime_error(fmt::format("{}: qemu-img failed ({}) with output:\n{}", + custom_error_prefix, process_state.failure_message(), - qemuimg_process->read_all_standard_error())); + process->read_all_standard_error())); } + + return process; +} + +void mp::backend::resize_instance_image(const MemorySize& disk_space, const mp::Path& image_path) +{ + auto disk_size = QString::number(disk_space.in_bytes()); // format documented in `man qemu-img` (look for "size") + QStringList qemuimg_parameters{{"resize", image_path, disk_size}}; + + checked_exec_qemu_img(std::make_unique(qemuimg_parameters, "", image_path), + "Cannot resize instance image", + mp::image_resize_timeout); } mp::Path mp::backend::convert_to_qcow_if_necessary(const mp::Path& image_path) @@ -52,17 +66,9 @@ mp::Path mp::backend::convert_to_qcow_if_necessary(const mp::Path& image_path) // TODO: we could support converting from other the image formats that qemu-img can deal with const auto qcow2_path{image_path + ".qcow2"}; - auto qemuimg_info_spec = - std::make_unique(QStringList{"info", "--output=json", image_path}, image_path); - auto qemuimg_info_process = mp::platform::make_process(std::move(qemuimg_info_spec)); - - auto process_state = qemuimg_info_process->execute(); - if (!process_state.completed_successfully()) - { - throw std::runtime_error(fmt::format("Cannot read image format: qemu-img failed ({}) with output:\n{}", - process_state.failure_message(), - qemuimg_info_process->read_all_standard_error())); - } + auto qemuimg_info_process = checked_exec_qemu_img( + std::make_unique(QStringList{"info", "--output=json", image_path}, image_path), + "Cannot read image format"); auto image_info = qemuimg_info_process->read_all_standard_output(); auto image_record = QJsonDocument::fromJson(QString(image_info).toUtf8(), nullptr).object(); @@ -71,15 +77,8 @@ mp::Path mp::backend::convert_to_qcow_if_necessary(const mp::Path& image_path) { auto qemuimg_convert_spec = std::make_unique( QStringList{"convert", "-p", "-O", "qcow2", image_path, qcow2_path}, image_path, qcow2_path); - auto qemuimg_convert_process = mp::platform::make_process(std::move(qemuimg_convert_spec)); - process_state = qemuimg_convert_process->execute(mp::image_resize_timeout); - - if (!process_state.completed_successfully()) - { - throw std::runtime_error( - fmt::format("Failed to convert image format: qemu-img failed ({}) with output:\n{}", - process_state.failure_message(), qemuimg_convert_process->read_all_standard_error())); - } + auto qemuimg_convert_process = + checked_exec_qemu_img(std::move(qemuimg_convert_spec), "Failed to convert image format"); return qcow2_path; } else @@ -87,3 +86,18 @@ mp::Path mp::backend::convert_to_qcow_if_necessary(const mp::Path& image_path) return image_path; } } + +void mp::backend::amend_to_qcow2_v3(const mp::Path& image_path) +{ + checked_exec_qemu_img( + std::make_unique(QStringList{"amend", "-o", "compat=1.1", image_path}, image_path)); +} + +bool mp::backend::instance_image_has_snapshot(const mp::Path& image_path, QString snapshot_tag) +{ + auto process = checked_exec_qemu_img( + std::make_unique(QStringList{"snapshot", "-l", image_path}, image_path)); + + QRegularExpression regex{snapshot_tag.append(R"(\s)")}; + return QString{process->read_all_standard_output()}.contains(regex); +} diff --git a/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.h b/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.h index 8872ba7f5d5..6d7b89e0b98 100644 --- a/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.h +++ b/src/platform/backends/shared/qemu_img_utils/qemu_img_utils.h @@ -19,15 +19,25 @@ #define MULTIPASS_QEMU_IMG_UTILS_H #include +#include + +#include namespace multipass { class MemorySize; +class QemuImgProcessSpec; namespace backend { +Process::UPtr checked_exec_qemu_img(std::unique_ptr spec, + const std::string& custom_error_prefix = "Internal error", + std::optional timeout = std::nullopt); void resize_instance_image(const MemorySize& disk_space, const multipass::Path& image_path); Path convert_to_qcow_if_necessary(const Path& image_path); +void amend_to_qcow2_v3(const Path& image_path); +bool instance_image_has_snapshot(const Path& image_path, QString snapshot_tag); + } // namespace backend } // namespace multipass #endif // MULTIPASS_QEMU_IMG_UTILS_H diff --git a/src/process/qemuimg_process_spec.cpp b/src/process/qemuimg_process_spec.cpp index 131e77cc24a..69c28fa8996 100644 --- a/src/process/qemuimg_process_spec.cpp +++ b/src/process/qemuimg_process_spec.cpp @@ -45,6 +45,7 @@ QString mp::QemuImgProcessSpec::apparmor_profile() const profile %1 flags=(attach_disconnected) { #include + capability ipc_lock, capability dac_read_search, %2 @@ -82,7 +83,7 @@ profile %1 flags=(attach_disconnected) { } if (!source_image.isEmpty()) - images.append(QString(" %1 rk,\n").arg(source_image)); + images.append(QString(" %1 rwk,\n").arg(source_image)); // allow amending to qcow2 v3 if (!target_image.isEmpty()) images.append(QString(" %1 rwk,\n").arg(target_image)); diff --git a/src/rpc/multipass.proto b/src/rpc/multipass.proto index 20cecf12ff2..06ca35e8a13 100644 --- a/src/rpc/multipass.proto +++ b/src/rpc/multipass.proto @@ -16,6 +16,8 @@ syntax = "proto3"; package multipass; +import "google/protobuf/timestamp.proto"; + service Rpc { rpc create (stream LaunchRequest) returns (stream LaunchReply); rpc launch (stream LaunchRequest) returns (stream LaunchReply); @@ -39,6 +41,8 @@ service Rpc { rpc set (stream SetRequest) returns (stream SetReply); rpc keys (stream KeysRequest) returns (stream KeysReply); rpc authenticate (stream AuthenticateRequest) returns (stream AuthenticateReply); + rpc snapshot (stream SnapshotRequest) returns (stream SnapshotReply); + rpc restore (stream RestoreRequest) returns (stream RestoreReply); } message LaunchRequest { @@ -156,14 +160,16 @@ message FindReply { string log_line = 5; } -message InstanceNames { - repeated string instance_name = 1; +message InstanceSnapshotPair { + string instance_name = 1; + optional string snapshot_name = 2; // if this is present, the msg specifies a snapshot; otherwise, an instance } message InfoRequest { - InstanceNames instance_names = 1; - int32 verbosity_level = 2; - bool no_runtime_information = 3; + repeated InstanceSnapshotPair instance_snapshot_pairs = 1; + int32 verbosity_level = 3; + bool no_runtime_information = 4; + bool snapshots = 5; } message IdMap { @@ -201,30 +207,55 @@ message InstanceStatus { Status status = 1; } -message InfoReply { - message Info { - string name = 1; - InstanceStatus instance_status = 2; - string image_release = 3; - string current_release = 4; - string id = 5; - string load = 6; - string memory_usage = 7; - string memory_total = 8; - string disk_usage = 9; - string disk_total = 10; - repeated string ipv4 = 11; - repeated string ipv6 = 12; - MountInfo mount_info = 13; - string cpu_count = 14; +message InstanceDetails { + string image_release = 1; + string current_release = 2; + string id = 3; + string load = 4; + string memory_usage = 5; + string disk_usage = 6; + repeated string ipv4 = 7; + repeated string ipv6 = 8; + int32 num_snapshots = 9; +} + +message SnapshotFundamentals { + string snapshot_name = 1; + string parent = 2; + string comment = 3; + google.protobuf.Timestamp creation_timestamp = 4; +} + +message SnapshotDetails { + SnapshotFundamentals fundamentals = 1; + string size = 2; + repeated string children = 4; +} + +message DetailedInfoItem { + string name = 1; + InstanceStatus instance_status = 2; + string memory_total = 3; + string disk_total = 4; + string cpu_count = 5; + MountInfo mount_info = 6; + + oneof extra_info { + InstanceDetails instance_info = 7; + SnapshotDetails snapshot_info = 8; } - repeated Info info = 1; - string log_line = 2; +} + +message InfoReply { + repeated DetailedInfoItem details = 1; + bool snapshots = 2; // useful to determine what entity (instance/snapshot) was absent when details are empty + string log_line = 3; } message ListRequest { int32 verbosity_level = 1; - bool request_ipv4 = 2; + bool snapshots = 2; + bool request_ipv4 = 3; } message ListVMInstance { @@ -235,12 +266,29 @@ message ListVMInstance { string current_release = 5; } -message ListReply { +message ListVMSnapshot { + string name = 1; + SnapshotFundamentals fundamentals = 2; +} + +message InstancesList { repeated ListVMInstance instances = 1; - string log_line = 2; - UpdateInfo update_info = 3; } +message SnapshotsList { + repeated ListVMSnapshot snapshots = 1; +} + +message ListReply { + oneof list_contents + { + InstancesList instance_list = 1; + SnapshotsList snapshot_list = 2; + } + + string log_line = 3; + UpdateInfo update_info = 4; +} message NetworksRequest { int32 verbosity_level = 1; @@ -288,6 +336,10 @@ message PingRequest { message PingReply { } +message InstanceNames { + repeated string instance_name = 1; +} + message RecoverRequest { InstanceNames instance_names = 1; int32 verbosity_level = 2; @@ -373,7 +425,7 @@ message RestartReply { } message DeleteRequest { - InstanceNames instance_names = 1; + repeated InstanceSnapshotPair instance_snapshot_pairs = 1; bool purge = 2; int32 verbosity_level = 3; } @@ -440,3 +492,28 @@ message AuthenticateRequest { message AuthenticateReply { string log_line = 1; } + +message SnapshotRequest { + string instance = 1; + string snapshot = 2; + string comment = 3; + int32 verbosity_level = 4; +} + +message SnapshotReply { + string snapshot = 1; // automatically generated unless specifically requested + string log_line = 2; +} + +message RestoreRequest { + string instance = 1; + string snapshot = 2; + bool destructive = 3; + int32 verbosity_level = 4; +} + +message RestoreReply { + string log_line = 1; + string reply_message = 2; + bool confirm_destructive = 3; +} diff --git a/src/sshfs_mount/sshfs_mount_handler.cpp b/src/sshfs_mount/sshfs_mount_handler.cpp index 76f4c679c78..62693f6b248 100644 --- a/src/sshfs_mount/sshfs_mount_handler.cpp +++ b/src/sshfs_mount/sshfs_mount_handler.cpp @@ -111,22 +111,25 @@ catch (const mp::ExitlessSSHProcessException&) namespace multipass { -SSHFSMountHandler::SSHFSMountHandler(VirtualMachine* vm, const SSHKeyProvider* ssh_key_provider, - const std::string& target, const VMMount& mount) - : MountHandler{vm, ssh_key_provider, target, mount.source_path}, +SSHFSMountHandler::SSHFSMountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + const std::string& target, + VMMount mount_spec) + : MountHandler{vm, ssh_key_provider, std::move(mount_spec), target}, process{nullptr}, config{"", 0, vm->ssh_username(), vm->vm_name, ssh_key_provider->private_key_as_base64(), - mount.source_path, + source, target, - mount.gid_mappings, - mount.uid_mappings} + this->mount_spec.gid_mappings, + this->mount_spec.uid_mappings} { - mpl::log(mpl::Level::info, category, - fmt::format("initializing mount {} => {} in '{}'", mount.source_path, target, vm->vm_name)); + mpl::log(mpl::Level::info, + category, + fmt::format("initializing mount {} => {} in '{}'", this->mount_spec.source_path, target, vm->vm_name)); } bool SSHFSMountHandler::is_active() diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index ced4a8824b3..cd5d0e4d2a2 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -21,7 +21,8 @@ function(add_target TARGET_NAME) standard_paths.cpp timer.cpp utils.cpp - vm_image_vault_utils.cpp) + vm_image_vault_utils.cpp + vm_mount.cpp) target_link_libraries(${TARGET_NAME} cert diff --git a/src/utils/file_ops.cpp b/src/utils/file_ops.cpp index e0b7560f897..d97f881e1b4 100644 --- a/src/utils/file_ops.cpp +++ b/src/utils/file_ops.cpp @@ -34,6 +34,14 @@ bool mp::FileOps::isReadable(const QDir& dir) const return dir.isReadable(); } +QFileInfoList multipass::FileOps::entryInfoList(const QDir& dir, + const QStringList& nameFilters, + QDir::Filters filters, + QDir::SortFlags sort) const +{ + return dir.entryInfoList(nameFilters, filters, sort); +} + bool mp::FileOps::mkpath(const QDir& dir, const QString& dirName) const { return dir.mkpath(dirName); diff --git a/src/utils/json_utils.cpp b/src/utils/json_utils.cpp index 898a02a309a..8e73568abb9 100644 --- a/src/utils/json_utils.cpp +++ b/src/utils/json_utils.cpp @@ -18,23 +18,40 @@ */ #include +#include #include -#include #include +#include + +#include namespace mp = multipass; -void mp::write_json(const QJsonObject& root, QString file_name) +mp::JsonUtils::JsonUtils(const Singleton::PrivatePass& pass) noexcept : Singleton{pass} +{ +} + +void mp::JsonUtils::write_json(const QJsonObject& root, QString file_name) const { - QJsonDocument doc{root}; - auto raw_json = doc.toJson(); - QFile db_file{file_name}; - MP_FILEOPS.open(db_file, QIODevice::ReadWrite | QIODevice::Truncate); - MP_FILEOPS.write(db_file, raw_json); + auto dir = QFileInfo(file_name).absoluteDir(); + if (!MP_FILEOPS.mkpath(dir, ".")) + throw std::runtime_error(fmt::format("Could not create path '{}'", dir.absolutePath())); + + QSaveFile db_file{file_name}; + if (!MP_FILEOPS.open(db_file, QIODevice::WriteOnly)) + throw std::runtime_error{fmt::format("Could not open transactional file for writing; filename: {}", file_name)}; + + if (MP_FILEOPS.write(db_file, QJsonDocument{root}.toJson()) == -1) + throw std::runtime_error{fmt::format("Could not write json to transactional file; filename: {}; error: {}", + file_name, + db_file.errorString())}; + + if (!MP_FILEOPS.commit(db_file)) + throw std::runtime_error{fmt::format("Could not commit transactional file; filename: {}", file_name)}; } -std::string mp::json_to_string(const QJsonObject& root) +std::string mp::JsonUtils::json_to_string(const QJsonObject& root) const { // The function name toJson() is shockingly wrong, for it converts an actual JsonDocument to a QByteArray. return QJsonDocument(root).toJson().toStdString(); diff --git a/src/utils/memory_size.cpp b/src/utils/memory_size.cpp index 925e2648c26..b8eb25338e9 100644 --- a/src/utils/memory_size.cpp +++ b/src/utils/memory_size.cpp @@ -79,7 +79,7 @@ long long in_bytes(const std::string& mem_value) } } // namespace -mp::MemorySize::MemorySize() : bytes{0LL} +mp::MemorySize::MemorySize() noexcept : bytes{0LL} { } diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 7668009d322..c213efd1325 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -237,13 +238,6 @@ std::string mp::utils::to_cmd(const std::vector& args, QuoteType qu return cmd; } -std::string& mp::utils::trim_end(std::string& s, std::function filter) -{ - auto rev_it = std::find_if_not(s.rbegin(), s.rend(), filter); - s.erase(rev_it.base(), s.end()); - return s; -} - std::string& mp::utils::trim_newline(std::string& s) { assert(!s.empty() && '\n' == s.back()); @@ -472,12 +466,12 @@ QString mp::utils::make_uuid(const std::optional& seed) return uuid.toString(QUuid::WithoutBraces); } -std::string mp::utils::contents_of(const multipass::Path& file_path) +std::string mp::utils::contents_of(const multipass::Path& file_path) // TODO this should protect against long contents { const std::string name{file_path.toStdString()}; std::ifstream in(name, std::ios::in | std::ios::binary); if (!in) - throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", name, strerror(errno), errno)); + throw FileOpenFailedException(name); std::stringstream stream; stream << in.rdbuf(); @@ -688,3 +682,13 @@ void mp::utils::set_owner_for(mp::SSHSession& session, const std::string& root, fmt::format("sudo /bin/bash -c 'cd \"{}\" && chown -R {}:{} \"{}\"'", root, vm_user, vm_group, relative_target.substr(0, relative_target.find_first_of('/')))); } + +mp::Path mp::Utils::derive_instances_dir(const mp::Path& data_dir, + const mp::Path& backend_directory_name, + const mp::Path& instances_subdir) const +{ + if (backend_directory_name.isEmpty()) + return QDir(data_dir).filePath(instances_subdir); + else + return QDir(QDir(data_dir).filePath(backend_directory_name)).filePath(instances_subdir); +} diff --git a/src/utils/vm_mount.cpp b/src/utils/vm_mount.cpp new file mode 100644 index 00000000000..b798eed92b1 --- /dev/null +++ b/src/utils/vm_mount.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +namespace mp = multipass; + +namespace +{ +mp::VMMount parse_json(const QJsonObject& json) +{ + mp::id_mappings uid_mappings; + mp::id_mappings gid_mappings; + auto source_path = json["source_path"].toString().toStdString(); + + for (const QJsonValueRef uid_entry : json["uid_mappings"].toArray()) + { + uid_mappings.push_back( + {uid_entry.toObject()["host_uid"].toInt(), uid_entry.toObject()["instance_uid"].toInt()}); + } + + for (const QJsonValueRef gid_entry : json["gid_mappings"].toArray()) + { + gid_mappings.push_back( + {gid_entry.toObject()["host_gid"].toInt(), gid_entry.toObject()["instance_gid"].toInt()}); + } + + uid_mappings = mp::unique_id_mappings(uid_mappings); + gid_mappings = mp::unique_id_mappings(gid_mappings); + auto mount_type = mp::VMMount::MountType(json["mount_type"].toInt()); + + return mp::VMMount{std::move(source_path), std::move(gid_mappings), std::move(uid_mappings), mount_type}; +} +} // namespace + +mp::VMMount::VMMount(const std::string& sourcePath, + id_mappings gidMappings, + id_mappings uidMappings, + MountType mountType) + : source_path(sourcePath), + gid_mappings(std::move(gidMappings)), + uid_mappings(std::move(uidMappings)), + mount_type(mountType) +{ +} + +mp::VMMount::VMMount(const QJsonObject& json) : VMMount{parse_json(json)} // delegate on copy ctor +{ +} + +QJsonObject mp::VMMount::serialize() const +{ + QJsonObject ret; + ret.insert("source_path", QString::fromStdString(source_path)); + + QJsonArray uid_mappings_json; + + for (const auto& map : uid_mappings) + { + QJsonObject map_entry; + map_entry.insert("host_uid", map.first); + map_entry.insert("instance_uid", map.second); + + uid_mappings_json.append(map_entry); + } + + ret.insert("uid_mappings", uid_mappings_json); + + QJsonArray gid_mappings_json; + + for (const auto& map : gid_mappings) + { + QJsonObject map_entry; + map_entry.insert("host_gid", map.first); + map_entry.insert("instance_gid", map.second); + + gid_mappings_json.append(map_entry); + } + + ret.insert("gid_mappings", gid_mappings_json); + + ret.insert("mount_type", static_cast(mount_type)); + return ret; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 52bd407f195..0bc8f6050f8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,7 +36,7 @@ add_executable(multipass_tests daemon_test_fixture.cpp file_operations.cpp image_host_remote_count.cpp - json_utils.cpp + json_test_utils.cpp main.cpp mischievous_url_downloader.cpp mock_logger.cpp diff --git a/tests/blueprint_test_lambdas.cpp b/tests/blueprint_test_lambdas.cpp index 0ea7bb8ede7..304efd08c08 100644 --- a/tests/blueprint_test_lambdas.cpp +++ b/tests/blueprint_test_lambdas.cpp @@ -29,17 +29,27 @@ #include "common.h" #include "stub_virtual_machine.h" #include "stub_vm_image_vault.h" +#include "temp_dir.h" namespace mp = multipass; namespace mpt = multipass::test; -std::function)> +std::function, + const mp::Path&)> mpt::fetch_image_lambda(const std::string& release, const std::string& remote, const bool must_have_checksum) { - return [&release, &remote, must_have_checksum]( - const mp::FetchType& fetch_type, const mp::Query& query, const mp::VMImageVault::PrepareAction& prepare, - const mp::ProgressMonitor& monitor, const bool unlock, const std::optional& checksum) { + return [&release, &remote, must_have_checksum](const mp::FetchType& fetch_type, + const mp::Query& query, + const mp::VMImageVault::PrepareAction& prepare, + const mp::ProgressMonitor& monitor, + const bool unlock, + const std::optional& checksum, + const mp::Path& save_dir) { EXPECT_EQ(query.release, release); if (remote.empty()) { @@ -55,7 +65,7 @@ mpt::fetch_image_lambda(const std::string& release, const std::string& remote, c EXPECT_NE(checksum, std::nullopt); } - return mpt::StubVMImageVault().fetch_image(fetch_type, query, prepare, monitor, unlock, checksum); + return mpt::StubVMImageVault().fetch_image(fetch_type, query, prepare, monitor, unlock, checksum, save_dir); }; } diff --git a/tests/blueprint_test_lambdas.h b/tests/blueprint_test_lambdas.h index 345b38846b2..f046c6d92df 100644 --- a/tests/blueprint_test_lambdas.h +++ b/tests/blueprint_test_lambdas.h @@ -38,8 +38,13 @@ class MemorySize; namespace test { -std::function)> +std::function, + const multipass::Path&)> fetch_image_lambda(const std::string& release, const std::string& remote, const bool must_have_checksum = false); std::function diff --git a/tests/json_utils.cpp b/tests/json_test_utils.cpp similarity index 99% rename from tests/json_utils.cpp rename to tests/json_test_utils.cpp index 0e2bd264c08..1796b245dbe 100644 --- a/tests/json_utils.cpp +++ b/tests/json_test_utils.cpp @@ -19,7 +19,7 @@ #include "common.h" #include "file_operations.h" -#include "json_utils.h" +#include "json_test_utils.h" #include diff --git a/tests/json_utils.h b/tests/json_test_utils.h similarity index 89% rename from tests/json_utils.h rename to tests/json_test_utils.h index 1385977fea2..c44b53c5fdb 100644 --- a/tests/json_utils.h +++ b/tests/json_test_utils.h @@ -15,16 +15,16 @@ * */ -#ifndef MULTIPASS_JSON_UTILS_H -#define MULTIPASS_JSON_UTILS_H +#ifndef MULTIPASS_JSON_TEST_UTILS_H +#define MULTIPASS_JSON_TEST_UTILS_H #include "temp_dir.h" #include - -#include +#include #include +#include #include #include @@ -43,4 +43,4 @@ void check_interfaces_in_json(const QString& file, const std::string& mac, void check_mounts_in_json(const QString& file, std::unordered_map& mounts); -#endif // MULTIPASS_JSON_UTILS_H +#endif // MULTIPASS_JSON_TEST_UTILS_H diff --git a/tests/lxd/test_lxd_backend.cpp b/tests/lxd/test_lxd_backend.cpp index 4cf7ac838b9..77641ed4b9d 100644 --- a/tests/lxd/test_lxd_backend.cpp +++ b/tests/lxd/test_lxd_backend.cpp @@ -81,6 +81,7 @@ struct LXDBackend : public Test mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); mpt::TempDir data_dir; + mpt::TempDir instance_dir; std::unique_ptr> mock_network_access_manager; QUrl base_url{"unix:///foo@1.0"}; const QString default_storage_pool{"default"}; @@ -445,8 +446,13 @@ TEST_F(LXDBackend, creates_in_stopped_state) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_TRUE(vm_created); EXPECT_EQ(machine.current_state(), mp::VirtualMachine::State::stopped); @@ -493,8 +499,13 @@ TEST_F(LXDBackend, machine_persists_and_sets_state_on_start) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_CALL(mock_monitor, persist_state_for(_, _)).Times(2); machine.start(); @@ -543,8 +554,13 @@ TEST_F(LXDBackend, machine_persists_and_sets_state_on_shutdown) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_CALL(mock_monitor, persist_state_for(_, _)).Times(2); machine.shutdown(); @@ -588,9 +604,13 @@ TEST_F(LXDBackend, machine_persists_internal_stopped_state_on_destruction) }); { - mp::LXDVirtualMachine machine{ - default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; ASSERT_EQ(machine.state, mp::VirtualMachine::State::running); } // Simulate multipass exiting by having the vm destruct @@ -645,9 +665,13 @@ TEST_F(LXDBackend, machine_does_not_update_state_in_dtor) // create in its own scope so the dtor is called { - mp::LXDVirtualMachine machine{ - default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } EXPECT_TRUE(vm_shutdown); @@ -703,9 +727,13 @@ TEST_F(LXDBackend, machineLogsNotFoundExceptionInDtor) // create in its own scope so the dtor is called { - mp::LXDVirtualMachine machine{ - default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; machine.shutdown(); } @@ -753,9 +781,13 @@ TEST_F(LXDBackend, does_not_call_stop_when_snap_refresh_is_detected) // create in its own scope so the dtor is called { - mp::LXDVirtualMachine machine{ - default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } EXPECT_FALSE(stop_requested); @@ -799,9 +831,13 @@ TEST_F(LXDBackend, calls_stop_when_snap_refresh_does_not_exist) // create in its own scope so the dtor is called { - mp::LXDVirtualMachine machine{ - default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } EXPECT_TRUE(stop_requested); @@ -884,8 +920,13 @@ TEST_F(LXDBackend, posts_expected_data_when_creating_instance) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } TEST_F(LXDBackend, prepare_source_image_does_not_modify) @@ -954,10 +995,11 @@ TEST_F(LXDBackend, unimplemented_functions_logs_trace_message) const std::string name{"foo"}; - EXPECT_CALL( - *logger_scope.mock_logger, - log(Eq(mpl::Level::trace), mpt::MockLogger::make_cstring_matcher(StrEq("lxd factory")), - mpt::MockLogger::make_cstring_matcher(StrEq(fmt::format("No resources to remove for \"{}\"", name))))); + EXPECT_CALL(*logger_scope.mock_logger, + log(Eq(mpl::Level::trace), + mpt::MockLogger::make_cstring_matcher(StrEq("lxd factory")), + mpt::MockLogger::make_cstring_matcher( + StrEq(fmt::format("No further resources to remove for \"{}\"", name))))); EXPECT_CALL(*logger_scope.mock_logger, log(Eq(mpl::Level::trace), mpt::MockLogger::make_cstring_matcher(StrEq("lxd factory")), @@ -1097,8 +1139,13 @@ TEST_P(LXDNetworkInfoSuite, returns_expected_network_info) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_EQ(machine.management_ipv4(mpt::StubSSHKeyProvider()), "10.217.27.168"); EXPECT_TRUE(machine.ipv6().empty()); @@ -1142,8 +1189,13 @@ TEST_F(LXDBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_THROW(machine.ssh_hostname(std::chrono::milliseconds(1)), std::runtime_error); EXPECT_EQ(machine.state, mp::VirtualMachine::State::unknown); @@ -1180,8 +1232,13 @@ TEST_F(LXDBackend, no_ip_address_returns_unknown) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_EQ(machine.management_ipv4(mpt::StubSSHKeyProvider()), "UNKNOWN"); } @@ -1523,8 +1580,13 @@ TEST_F(LXDBackend, unsupported_suspend_throws) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; MP_EXPECT_THROW_THAT(machine.suspend(), std::runtime_error, mpt::match_what(StrEq("suspend is currently not supported"))); @@ -1556,8 +1618,13 @@ TEST_F(LXDBackend, start_while_frozen_unfreezes) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_CALL(*logger_scope.mock_logger, log(Eq(mpl::Level::info), mpt::MockLogger::make_cstring_matcher(StrEq("pied-piper-valley")), @@ -1584,8 +1651,13 @@ TEST_F(LXDBackend, shutdown_while_stopped_does_nothing_and_logs_debug) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; ASSERT_EQ(machine.current_state(), mp::VirtualMachine::State::stopped); @@ -1616,8 +1688,13 @@ TEST_F(LXDBackend, shutdown_while_frozen_does_nothing_and_logs_info) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, mock_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + mock_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; ASSERT_EQ(machine.current_state(), mp::VirtualMachine::State::suspended); @@ -1667,8 +1744,13 @@ TEST_F(LXDBackend, ensure_vm_running_does_not_throw_starting) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; machine.start(); @@ -1720,8 +1802,13 @@ TEST_F(LXDBackend, shutdown_while_starting_throws_and_sets_correct_state) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; machine.start(); @@ -1773,8 +1860,13 @@ TEST_F(LXDBackend, start_failure_while_starting_throws_and_sets_correct_state) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; machine.start(); @@ -1827,8 +1919,13 @@ TEST_F(LXDBackend, reboots_while_starting_does_not_throw_and_sets_correct_state) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; machine.start(); @@ -1850,8 +1947,13 @@ TEST_F(LXDBackend, current_state_connection_error_logs_warning_and_sets_unknown_ throw mp::LocalSocketConnectionException(exception_message); }); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_CALL(*logger_scope.mock_logger, log(Eq(mpl::Level::warning), mpt::MockLogger::make_cstring_matcher(StrEq("pied-piper-valley")), @@ -1904,8 +2006,13 @@ TEST_P(LXDInstanceStatusTestSuite, lxd_state_returns_expected_VirtualMachine_sta } } - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; EXPECT_EQ(machine.current_state(), expected_state); } @@ -2136,8 +2243,13 @@ TEST_F(LXDBackend, posts_extra_network_devices) auto json_matcher = ResultOf(&extract_devices, devices_matcher); setup_vm_creation_expectations(*mock_network_access_manager, request_data_matcher(json_matcher)); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } TEST_F(LXDBackend, posts_network_data_config_if_available) @@ -2154,8 +2266,13 @@ TEST_F(LXDBackend, posts_network_data_config_if_available) setup_vm_creation_expectations(*mock_network_access_manager, request_data_matcher(json_matcher)); - mp::LXDVirtualMachine machine{default_description, stub_monitor, mock_network_access_manager.get(), base_url, - bridge_name, default_storage_pool}; + mp::LXDVirtualMachine machine{default_description, + stub_monitor, + mock_network_access_manager.get(), + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; } namespace diff --git a/tests/lxd/test_lxd_image_vault.cpp b/tests/lxd/test_lxd_image_vault.cpp index 79e5bd0b127..cea8b0ac051 100644 --- a/tests/lxd/test_lxd_image_vault.cpp +++ b/tests/lxd/test_lxd_image_vault.cpp @@ -70,6 +70,7 @@ struct LXDImageVault : public Test mp::Query default_query{instance_name, "xenial", false, "", mp::Query::Type::Alias}; mpt::StubURLDownloader stub_url_downloader; mpt::TempDir cache_dir; + mpt::TempDir save_dir; }; } // namespace @@ -94,8 +95,13 @@ TEST_F(LXDImageVault, instance_exists_fetch_returns_expected_image_info) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, - false, std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_EQ(image.id, mpt::default_id); EXPECT_EQ(image.original_release, "18.04 LTS"); @@ -122,8 +128,13 @@ TEST_F(LXDImageVault, instance_exists_custom_image_returns_expected_image_info) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, - false, std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_EQ(image.id, "6937ddd3f4c3329182855843571fc91ae4fee24e8e0eb0f7cdcf2c22feed4dab"); EXPECT_EQ(image.original_release, "Snapcraft builder for Core 20"); @@ -151,8 +162,13 @@ TEST_F(LXDImageVault, instance_exists_uses_cached_release_title) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, - false, std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_EQ(image.id, mpt::default_id); EXPECT_EQ(image.original_release, "Fake Title"); @@ -181,8 +197,13 @@ TEST_F(LXDImageVault, instance_exists_no_cached_release_title_info_for_fails) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, - false, std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_EQ(image.id, mpt::default_id); EXPECT_EQ(image.original_release, ""); @@ -211,8 +232,13 @@ TEST_F(LXDImageVault, returns_expected_info_with_valid_remote) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, - std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_EQ(image.id, mpt::default_id); EXPECT_EQ(image.original_release, "18.04 LTS"); @@ -241,7 +267,13 @@ TEST_F(LXDImageVault, throws_with_invalid_alias) base_url, cache_dir.path(), mp::days{0}}; MP_EXPECT_THROW_THAT( - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), + image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()), std::runtime_error, mpt::match_what( StrEq(fmt::format("Unable to find an image matching \"{}\" in remote \"{}\".", alias, "release")))); @@ -259,9 +291,15 @@ TEST_F(LXDImageVault, throws_with_invalid_remote) mp::LXDVMImageVault image_vault{hosts, &stub_url_downloader, mock_network_access_manager.get(), base_url, cache_dir.path(), mp::days{0}}; - MP_EXPECT_THROW_THAT( - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), - std::runtime_error, mpt::match_what(HasSubstr(fmt::format("Remote \'{}\' is not found.", remote)))); + MP_EXPECT_THROW_THAT(image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()), + std::runtime_error, + mpt::match_what(HasSubstr(fmt::format("Remote \'{}\' is not found.", remote)))); } TEST_F(LXDImageVault, does_not_download_if_image_exists) @@ -288,8 +326,13 @@ TEST_F(LXDImageVault, does_not_download_if_image_exists) mp::LXDVMImageVault image_vault{hosts, &stub_url_downloader, mock_network_access_manager.get(), base_url, cache_dir.path(), mp::days{0}}; - EXPECT_NO_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, - std::nullopt)); + EXPECT_NO_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); } TEST_F(LXDImageVault, instance_exists_missing_image_does_not_download_image) @@ -325,8 +368,13 @@ TEST_F(LXDImageVault, instance_exists_missing_image_does_not_download_image) base_url, cache_dir.path(), mp::days{0}}; mp::VMImage image; - EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, - false, std::nullopt)); + EXPECT_NO_THROW(image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_FALSE(download_requested); EXPECT_EQ(image.original_release, mpt::default_release_info); } @@ -352,8 +400,13 @@ TEST_F(LXDImageVault, requests_download_if_image_does_not_exist) mp::LXDVMImageVault image_vault{hosts, &stub_url_downloader, mock_network_access_manager.get(), base_url, cache_dir.path(), mp::days{0}}; - EXPECT_NO_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, - std::nullopt)); + EXPECT_NO_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); EXPECT_TRUE(download_requested); } @@ -380,8 +433,13 @@ TEST_F(LXDImageVault, sets_fingerprint_with_hash_query) base_url, cache_dir.path(), mp::days{0}}; const mp::Query query{"", "e3b0c44298fc1c1", false, "release", mp::Query::Type::Alias}; - EXPECT_NO_THROW( - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt)); + EXPECT_NO_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path())); } TEST_F(LXDImageVault, download_deletes_and_throws_on_cancel) @@ -419,9 +477,14 @@ TEST_F(LXDImageVault, download_deletes_and_throws_on_cancel) mp::LXDVMImageVault image_vault{hosts, &stub_url_downloader, mock_network_access_manager.get(), base_url, cache_dir.path(), mp::days{0}}; - EXPECT_THROW( - image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, monitor, false, std::nullopt), - mp::AbortedDownloadException); + EXPECT_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + monitor, + false, + std::nullopt, + save_dir.path()), + mp::AbortedDownloadException); EXPECT_TRUE(delete_requested); } @@ -457,9 +520,14 @@ TEST_F(LXDImageVault, percent_complete_returns_negative_on_metadata_download) mp::LXDVMImageVault image_vault{hosts, &stub_url_downloader, mock_network_access_manager.get(), base_url, cache_dir.path(), mp::days{0}}; - EXPECT_THROW( - image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, monitor, false, std::nullopt), - mp::AbortedDownloadException); + EXPECT_THROW(image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + monitor, + false, + std::nullopt, + save_dir.path()), + mp::AbortedDownloadException); } TEST_F(LXDImageVault, delete_requested_on_instance_remove) @@ -832,8 +900,13 @@ TEST_F(LXDImageVault, custom_image_found_returns_expected_info) base_url, cache_dir.path(), mp::days{0}}; const mp::Query query{"", "snapcraft", false, "release", mp::Query::Type::Alias}; - auto image = - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()); EXPECT_EQ(image.id, mpt::lxd_snapcraft_image_id); EXPECT_EQ(image.original_release, mpt::snapcraft_release_info); @@ -890,8 +963,13 @@ TEST_F(LXDImageVault, custom_image_downloads_and_creates_correct_upload) base_url, cache_dir.path(), mp::days{0}}; const mp::Query query{"", "custom", false, "release", mp::Query::Type::Alias}; - auto image = - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()); EXPECT_EQ(image.id, mpt::lxd_custom_image_id); EXPECT_EQ(image.original_release, mpt::custom_release_info); @@ -915,8 +993,13 @@ TEST_F(LXDImageVault, fetch_image_unable_to_connect_logs_error_and_returns_blank mpt::MockLogger::make_cstring_matcher( StrEq(fmt::format("{} - returning blank image info", exception_message))))); - auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, - std::nullopt); + auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()); EXPECT_TRUE(image.id.empty()); EXPECT_TRUE(image.original_release.empty()); @@ -1043,8 +1126,13 @@ TEST_F(LXDImageVault, http_based_image_downloads_and_creates_correct_upload) const std::string download_url{"http://www.foo.com/images/foo.img"}; const mp::Query query{"", download_url, false, "", mp::Query::Type::HttpDownload}; - auto image = - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()); EXPECT_EQ(image.id, "bc5a973bd6f2bef30658fb51177cf5e506c1d60958a4c97813ee26416dc368da"); @@ -1109,8 +1197,13 @@ TEST_F(LXDImageVault, file_based_fetch_copies_image_and_returns_expected_info) auto current_time = QDateTime::currentDateTime(); const mp::Query query{"", file.url().toStdString(), false, "", mp::Query::Type::LocalFile}; - auto image = - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto image = image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()); EXPECT_EQ(image.id, "bc5a973bd6f2bef30658fb51177cf5e506c1d60958a4c97813ee26416dc368da"); @@ -1131,9 +1224,15 @@ TEST_F(LXDImageVault, invalid_local_file_image_throws) const std::string missing_file{"/foo"}; const mp::Query query{"", fmt::format("file://{}", missing_file), false, "", mp::Query::Type::LocalFile}; - MP_EXPECT_THROW_THAT( - image_vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), - std::runtime_error, mpt::match_what(StrEq(fmt::format("Custom image `{}` does not exist.", missing_file)))); + MP_EXPECT_THROW_THAT(image_vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + save_dir.path()), + std::runtime_error, + mpt::match_what(StrEq(fmt::format("Custom image `{}` does not exist.", missing_file)))); } TEST_F(LXDImageVault, updateImagesThrowsOnMissingImage) diff --git a/tests/lxd/test_lxd_mount_handler.cpp b/tests/lxd/test_lxd_mount_handler.cpp index 8bd3efc103b..abb12969605 100644 --- a/tests/lxd/test_lxd_mount_handler.cpp +++ b/tests/lxd/test_lxd_mount_handler.cpp @@ -22,9 +22,11 @@ #include "tests/mock_virtual_machine.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" +#include "tests/temp_dir.h" #include "src/platform/backends/lxd/lxd_mount_handler.h" +#include #include #include #include @@ -165,8 +167,14 @@ TEST_F(LXDMountHandlerTestFixture, stopThrowsIfVMIsRunning) TEST_P(LXDMountHandlerInvalidGidUidParameterTests, mountWithGidOrUid) { - mp::LXDVirtualMachine lxd_vm{default_description, stub_monitor, &mock_network_access_manager, base_url, - bridge_name, default_storage_pool}; + mpt::TempDir instance_dir{}; + mp::LXDVirtualMachine lxd_vm{default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; const auto& [host_gid, instance_gid, host_uid, instance_uid] = GetParam(); const mp::VMMount vm_mount{ source_path, {{host_gid, instance_gid}}, {{host_uid, instance_uid}}, mp::VMMount::MountType::Native}; @@ -182,8 +190,14 @@ INSTANTIATE_TEST_SUITE_P(mountWithGidOrUidInstantiation, LXDMountHandlerInvalidG TEST_P(LXDMountHandlerValidGidUidParameterTests, mountWithGidOrUid) { - mp::LXDVirtualMachine lxd_vm{default_description, stub_monitor, &mock_network_access_manager, base_url, - bridge_name, default_storage_pool}; + mpt::TempDir instance_dir{}; + mp::LXDVirtualMachine lxd_vm{default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + instance_dir.path()}; const auto& [host_gid, host_uid] = GetParam(); const int default_instance_id = -1; const mp::VMMount vm_mount{source_path, diff --git a/tests/mock_client_rpc.h b/tests/mock_client_rpc.h index 97e41906230..35da28d0b3d 100644 --- a/tests/mock_client_rpc.h +++ b/tests/mock_client_rpc.h @@ -188,6 +188,30 @@ class MockRpcStub : public multipass::Rpc::StubInterface (override)); MOCK_METHOD((grpc::ClientAsyncReaderWriterInterface*), PrepareAsyncauthenticateRaw, (grpc::ClientContext * context, grpc::CompletionQueue* cq), (override)); + MOCK_METHOD((grpc::ClientReaderWriterInterface*), + snapshotRaw, + (grpc::ClientContext * context), + (override)); + MOCK_METHOD((grpc::ClientAsyncReaderWriterInterface*), + AsyncsnapshotRaw, + (grpc::ClientContext * context, grpc::CompletionQueue* cq, void* tag), + (override)); + MOCK_METHOD((grpc::ClientAsyncReaderWriterInterface*), + PrepareAsyncsnapshotRaw, + (grpc::ClientContext * context, grpc::CompletionQueue* cq), + (override)); + MOCK_METHOD((grpc::ClientReaderWriterInterface*), + restoreRaw, + (grpc::ClientContext * context), + (override)); + MOCK_METHOD((grpc::ClientAsyncReaderWriterInterface*), + AsyncrestoreRaw, + (grpc::ClientContext * context, grpc::CompletionQueue* cq, void* tag), + (override)); + MOCK_METHOD((grpc::ClientAsyncReaderWriterInterface*), + PrepareAsyncrestoreRaw, + (grpc::ClientContext * context, grpc::CompletionQueue* cq), + (override)); }; } // namespace multipass::test diff --git a/tests/mock_json_utils.h b/tests/mock_json_utils.h new file mode 100644 index 00000000000..7dd2a838693 --- /dev/null +++ b/tests/mock_json_utils.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_MOCK_JSON_UTILS_H +#define MULTIPASS_MOCK_JSON_UTILS_H + +#include "common.h" +#include "mock_singleton_helpers.h" + +#include + +namespace multipass::test +{ +class MockJsonUtils : public JsonUtils +{ +public: + using JsonUtils::JsonUtils; + + MOCK_METHOD(void, write_json, (const QJsonObject&, QString), (const, override)); + MOCK_METHOD(std::string, json_to_string, (const QJsonObject& root), (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockJsonUtils, JsonUtils); +}; +} // namespace multipass::test + +#endif // MULTIPASS_MOCK_JSON_UTILS_H diff --git a/tests/mock_virtual_machine.h b/tests/mock_virtual_machine.h index 81df94b4408..76e63ad328f 100644 --- a/tests/mock_virtual_machine.h +++ b/tests/mock_virtual_machine.h @@ -19,6 +19,7 @@ #define MULTIPASS_MOCK_VIRTUAL_MACHINE_H #include "common.h" +#include "temp_dir.h" #include #include @@ -34,7 +35,13 @@ template - MockVirtualMachineT(Args&&... args) : T{std::forward(args)...} + MockVirtualMachineT(Args&&... args) : MockVirtualMachineT{std::make_unique(), std::forward(args)...} + { + } + + template + MockVirtualMachineT(std::unique_ptr&& tmp_dir, Args&&... args) + : T{std::forward(args)..., tmp_dir->path()} { ON_CALL(*this, current_state()).WillByDefault(Return(multipass::VirtualMachine::State::off)); ON_CALL(*this, ssh_port()).WillByDefault(Return(42)); @@ -61,11 +68,29 @@ struct MockVirtualMachineT : public T MOCK_METHOD(void, ensure_vm_is_running, (), (override)); MOCK_METHOD(void, wait_until_ssh_up, (std::chrono::milliseconds, const SSHKeyProvider&), (override)); MOCK_METHOD(void, update_state, (), (override)); - MOCK_METHOD(void, update_cpus, (int num_cores), (override)); - MOCK_METHOD(void, resize_memory, (const MemorySize& new_size), (override)); - MOCK_METHOD(void, resize_disk, (const MemorySize& new_size), (override)); - MOCK_METHOD(std::unique_ptr, make_native_mount_handler, - (const SSHKeyProvider* ssh_key_provider, const std::string& target, const VMMount& mount), (override)); + MOCK_METHOD(void, update_cpus, (int), (override)); + MOCK_METHOD(void, resize_memory, (const MemorySize&), (override)); + MOCK_METHOD(void, resize_disk, (const MemorySize&), (override)); + MOCK_METHOD(std::unique_ptr, + make_native_mount_handler, + (const SSHKeyProvider*, const std::string&, const VMMount&), + (override)); + MOCK_METHOD(VirtualMachine::SnapshotVista, view_snapshots, (), (const, override, noexcept)); + MOCK_METHOD(int, get_num_snapshots, (), (const, override, noexcept)); + MOCK_METHOD(std::shared_ptr, get_snapshot, (const std::string&), (const, override)); + MOCK_METHOD(std::shared_ptr, get_snapshot, (int index), (const, override)); + MOCK_METHOD(std::shared_ptr, get_snapshot, (const std::string&), (override)); + MOCK_METHOD(std::shared_ptr, get_snapshot, (int index), (override)); + MOCK_METHOD(std::shared_ptr, + take_snapshot, + (const VMSpecs&, const std::string&, const std::string&), + (override)); + MOCK_METHOD(void, rename_snapshot, (const std::string& old_name, const std::string& new_name), (override)); + MOCK_METHOD(void, delete_snapshot, (const std::string& name), (override)); + MOCK_METHOD(void, restore_snapshot, (const std::string&, VMSpecs&), (override)); + MOCK_METHOD(void, load_snapshots, (), (override)); + MOCK_METHOD(std::vector, get_childrens_names, (const Snapshot*), (const, override)); + MOCK_METHOD(int, get_snapshot_count, (), (const, override)); }; using MockVirtualMachine = MockVirtualMachineT<>; diff --git a/tests/mock_virtual_machine_factory.h b/tests/mock_virtual_machine_factory.h index b178ea67412..37ad0d89d9d 100644 --- a/tests/mock_virtual_machine_factory.h +++ b/tests/mock_virtual_machine_factory.h @@ -40,8 +40,9 @@ struct MockVirtualMachineFactory : public VirtualMachineFactory MOCK_METHOD(VMImage, prepare_source_image, (const VMImage&), (override)); MOCK_METHOD(void, prepare_instance_image, (const VMImage&, const VirtualMachineDescription&), (override)); MOCK_METHOD(void, hypervisor_health_check, (), (override)); - MOCK_METHOD(QString, get_backend_directory_name, (), (override)); - MOCK_METHOD(QString, get_backend_version_string, (), (override)); + MOCK_METHOD(QString, get_backend_directory_name, (), (const, override)); + MOCK_METHOD(QString, get_instance_directory, (const std::string&), (const, override)); + MOCK_METHOD(QString, get_backend_version_string, (), (const, override)); MOCK_METHOD(VMImageVault::UPtr, create_image_vault, (std::vector, URLDownloader*, const Path&, const Path&, const days&), (override)); MOCK_METHOD(void, configure, (VirtualMachineDescription&), (override)); diff --git a/tests/mock_vm_image_vault.h b/tests/mock_vm_image_vault.h index 1241a19b831..0c85d83de5c 100644 --- a/tests/mock_vm_image_vault.h +++ b/tests/mock_vm_image_vault.h @@ -35,17 +35,23 @@ class MockVMImageVault : public VMImageVault public: MockVMImageVault() { - ON_CALL(*this, fetch_image(_, _, _, _, _, _)) - .WillByDefault([this](auto, auto, const PrepareAction& prepare, auto, auto, auto) { + ON_CALL(*this, fetch_image(_, _, _, _, _, _, _)) + .WillByDefault([this](auto, auto, const PrepareAction& prepare, auto, auto, auto, auto) { return VMImage{dummy_image.name(), {}, {}, {}, {}, {}}; }); ON_CALL(*this, has_record_for(_)).WillByDefault(Return(true)); ON_CALL(*this, minimum_image_size_for(_)).WillByDefault(Return(MemorySize{"1048576"})); }; - MOCK_METHOD(VMImage, fetch_image, - (const FetchType&, const Query&, const PrepareAction&, const ProgressMonitor&, const bool, - const std::optional&), + MOCK_METHOD(VMImage, + fetch_image, + (const FetchType&, + const Query&, + const PrepareAction&, + const ProgressMonitor&, + const bool, + const std::optional&, + const mp::Path&), (override)); MOCK_METHOD(void, remove, (const std::string&), (override)); MOCK_METHOD(bool, has_record_for, (const std::string&), (override)); diff --git a/tests/qemu/mock_qemu_platform.h b/tests/qemu/mock_qemu_platform.h index 9ae08016b6d..8a84e6b5fe1 100644 --- a/tests/qemu/mock_qemu_platform.h +++ b/tests/qemu/mock_qemu_platform.h @@ -41,7 +41,7 @@ struct MockQemuPlatform : public QemuPlatform MOCK_METHOD(void, platform_health_check, (), (override)); MOCK_METHOD(QStringList, vmstate_platform_args, (), (override)); MOCK_METHOD(QStringList, vm_platform_args, (const VirtualMachineDescription&), (override)); - MOCK_METHOD(QString, get_directory_name, (), (override)); + MOCK_METHOD(QString, get_directory_name, (), (const, override)); }; struct MockQemuPlatformFactory : public QemuPlatformFactory diff --git a/tests/qemu/test_qemu_backend.cpp b/tests/qemu/test_qemu_backend.cpp index 01e7ea4b4dd..bc582ed9d2c 100644 --- a/tests/qemu/test_qemu_backend.cpp +++ b/tests/qemu/test_qemu_backend.cpp @@ -49,8 +49,11 @@ namespace mpt = multipass::test; using namespace testing; namespace -{ // copied from QemuVirtualMachine implementation +{ +// copied from QemuVirtualMachine implementation constexpr auto suspend_tag = "suspend"; +// we need a whitespace to terminate the tag column in the fake output of qemu-img +const QByteArray fake_snapshot_list_with_suspend_tag = QByteArray{suspend_tag} + " "; } // namespace struct QemuBackend : public mpt::TestWithMockedBinPath @@ -77,6 +80,7 @@ struct QemuBackend : public mpt::TestWithMockedBinPath {}, {}}; mpt::TempDir data_dir; + mpt::TempDir instance_dir; const std::string tap_device{"tapfoo"}; const QString bridge_name{"dummy-bridge"}; const std::string subnet{"192.168.64"}; @@ -88,7 +92,7 @@ struct QemuBackend : public mpt::TestWithMockedBinPath mp::ProcessState exit_state; exit_state.exit_code = 0; ON_CALL(*process, execute(_)).WillByDefault(Return(exit_state)); - ON_CALL(*process, read_all_standard_output()).WillByDefault(Return(suspend_tag)); + ON_CALL(*process, read_all_standard_output()).WillByDefault(Return(fake_snapshot_list_with_suspend_tag)); } else if (process->program() == "iptables") { @@ -475,8 +479,6 @@ TEST_F(QemuBackend, verify_qemu_arguments_when_resuming_suspend_image_uses_metad TEST_F(QemuBackend, verify_qemu_arguments_from_metadata_are_used) { - constexpr auto suspend_tag = "suspend"; - EXPECT_CALL(*mock_qemu_platform_factory, make_qemu_platform(_)).WillOnce([this](auto...) { return std::move(mock_qemu_platform); }); @@ -487,7 +489,7 @@ TEST_F(QemuBackend, verify_qemu_arguments_from_metadata_are_used) mp::ProcessState exit_state; exit_state.exit_code = 0; EXPECT_CALL(*process, execute(_)).WillOnce(Return(exit_state)); - EXPECT_CALL(*process, read_all_standard_output()).WillOnce(Return(suspend_tag)); + EXPECT_CALL(*process, read_all_standard_output()).WillOnce(Return(fake_snapshot_list_with_suspend_tag)); } }; @@ -622,7 +624,7 @@ TEST_F(QemuBackend, ssh_hostname_returns_expected_value) return std::optional{expected_ip}; }); - mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor}; + mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -637,7 +639,7 @@ TEST_F(QemuBackend, gets_management_ip) EXPECT_CALL(mock_qemu_platform, get_ip_for(_)).WillOnce(Return(expected_ip)); - mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor}; + mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -651,7 +653,7 @@ TEST_F(QemuBackend, fails_to_get_management_ip_if_dnsmasq_does_not_return_an_ip) EXPECT_CALL(mock_qemu_platform, get_ip_for(_)).WillOnce(Return(std::nullopt)); - mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor}; + mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -665,7 +667,7 @@ TEST_F(QemuBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) ON_CALL(mock_qemu_platform, get_ip_for(_)).WillByDefault([](auto...) { return std::nullopt; }); - mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor}; + mp::QemuVirtualMachine machine{default_description, &mock_qemu_platform, stub_monitor, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -731,11 +733,13 @@ TEST_F(QemuBackend, get_backend_directory_name_calls_qemu_platform) bool get_directory_name_called{false}; const QString backend_dir_name{"foo"}; - EXPECT_CALL(*mock_qemu_platform, get_directory_name()).WillOnce([&get_directory_name_called, &backend_dir_name] { - get_directory_name_called = true; + EXPECT_CALL(*mock_qemu_platform, get_directory_name()) + .Times(2) + .WillRepeatedly([&get_directory_name_called, &backend_dir_name] { + get_directory_name_called = true; - return backend_dir_name; - }); + return backend_dir_name; + }); EXPECT_CALL(*mock_qemu_platform_factory, make_qemu_platform(_)).WillOnce([this](auto...) { return std::move(mock_qemu_platform); diff --git a/tests/stub_snapshot.h b/tests/stub_snapshot.h new file mode 100644 index 00000000000..f384903ffd3 --- /dev/null +++ b/tests/stub_snapshot.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_STUB_SNAPSHOT_H +#define MULTIPASS_STUB_SNAPSHOT_H + +#include +#include + +#include + +namespace multipass::test +{ +struct StubSnapshot : public Snapshot +{ + std::string get_name() const noexcept override + { + return {}; + } + + std::string get_comment() const noexcept override + { + return {}; + } + + QDateTime get_creation_timestamp() const noexcept override + { + return QDateTime{}; + } + + std::string get_parents_name() const override + { + return {}; + } + + std::shared_ptr get_parent() const noexcept override + { + return nullptr; + } + + std::shared_ptr get_parent() override + { + return nullptr; + } + + int get_index() const noexcept override + { + return 0; + } + + int get_parents_index() const override + { + return 0; + } + + int get_num_cores() const noexcept override + { + return 0; + } + + MemorySize get_mem_size() const noexcept override + { + return MemorySize{}; + } + + MemorySize get_disk_space() const noexcept override + { + return MemorySize{}; + } + + VirtualMachine::State get_state() const noexcept override + { + return VirtualMachine::State::off; + } + + const std::unordered_map& get_mounts() const noexcept override + { + return mounts; + } + + const QJsonObject& get_metadata() const noexcept override + { + return metadata; + } + + void set_name(const std::string&) override + { + } + + void set_comment(const std::string&) override + { + } + + void set_parent(std::shared_ptr) override + { + } + + void capture() override + { + } + + void erase() override + { + } + + void apply() override + { + } + + std::unordered_map mounts; + QJsonObject metadata; +}; +} // namespace multipass::test + +#endif // MULTIPASS_STUB_SNAPSHOT_H diff --git a/tests/stub_virtual_machine.h b/tests/stub_virtual_machine.h index 5ed0f25f5d5..3009b79d1a1 100644 --- a/tests/stub_virtual_machine.h +++ b/tests/stub_virtual_machine.h @@ -19,6 +19,9 @@ #define MULTIPASS_STUB_VIRTUAL_MACHINE_H #include "stub_mount_handler.h" +#include "stub_snapshot.h" +#include "temp_dir.h" + #include namespace multipass @@ -31,7 +34,12 @@ struct StubVirtualMachine final : public multipass::VirtualMachine { } - StubVirtualMachine(const std::string& name) : VirtualMachine{name} + StubVirtualMachine(const std::string& name) : StubVirtualMachine{name, std::make_unique()} + { + } + + StubVirtualMachine(const std::string& name, std::unique_ptr&& tmp_dir) + : VirtualMachine{name, tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} { } @@ -111,11 +119,76 @@ struct StubVirtualMachine final : public multipass::VirtualMachine { } - std::unique_ptr make_native_mount_handler(const SSHKeyProvider* ssh_key_provider, - const std::string& target, const VMMount& mount) override + std::unique_ptr make_native_mount_handler(const SSHKeyProvider*, + const std::string&, + const VMMount&) override { return std::make_unique(); } + + SnapshotVista view_snapshots() const override + { + return {}; + } + + int get_num_snapshots() const noexcept override + { + return 0; + } + + std::shared_ptr get_snapshot(const std::string&) const override + { + return {}; + } + + std::shared_ptr get_snapshot(const std::string&) override + { + return {}; + } + + std::shared_ptr get_snapshot(int) const override + { + return nullptr; + } + + std::shared_ptr get_snapshot(int) override + { + return nullptr; + } + + std::shared_ptr take_snapshot(const VMSpecs&, const std::string&, const std::string&) override + { + return {}; + } + + void rename_snapshot(const std::string& old_name, const std::string& new_name) override + { + } + + void delete_snapshot(const std::string&) override + { + } + + void restore_snapshot(const std::string& name, VMSpecs& specs) override + { + } + + void load_snapshots() override + { + } + + std::vector get_childrens_names(const Snapshot*) const override + { + return {}; + } + + int get_snapshot_count() const override + { + return 0; + } + + StubSnapshot snapshot; + std::unique_ptr tmp_dir; }; } // namespace test } // namespace multipass diff --git a/tests/stub_virtual_machine_factory.h b/tests/stub_virtual_machine_factory.h index 89a42e6e73a..93305b094d3 100644 --- a/tests/stub_virtual_machine_factory.h +++ b/tests/stub_virtual_machine_factory.h @@ -20,6 +20,7 @@ #include "stub_virtual_machine.h" #include "stub_vm_image_vault.h" +#include "temp_dir.h" #include @@ -29,13 +30,22 @@ namespace test { struct StubVirtualMachineFactory : public multipass::BaseVirtualMachineFactory { + StubVirtualMachineFactory() : StubVirtualMachineFactory{std::make_unique()} + { + } + + StubVirtualMachineFactory(std::unique_ptr&& tmp_dir) + : mp::BaseVirtualMachineFactory{tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} + { + } + multipass::VirtualMachine::UPtr create_virtual_machine(const multipass::VirtualMachineDescription&, multipass::VMStatusMonitor&) override { return std::make_unique(); } - void remove_resources_for(const std::string& name) override + void remove_resources_for_impl(const std::string& name) override { } @@ -58,12 +68,17 @@ struct StubVirtualMachineFactory : public multipass::BaseVirtualMachineFactory { } - QString get_backend_directory_name() override + QString get_backend_directory_name() const override { return {}; } - QString get_backend_version_string() override + QString get_instance_directory(const std::string& name) const override + { + return tmp_dir->path(); + } + + QString get_backend_version_string() const override { return "stub-5678"; } @@ -74,6 +89,8 @@ struct StubVirtualMachineFactory : public multipass::BaseVirtualMachineFactory { return std::make_unique(); } + + std::unique_ptr tmp_dir; }; } } diff --git a/tests/stub_vm_image_vault.h b/tests/stub_vm_image_vault.h index c8a7687ea22..71f5ccfbf20 100644 --- a/tests/stub_vm_image_vault.h +++ b/tests/stub_vm_image_vault.h @@ -28,9 +28,13 @@ namespace test { struct StubVMImageVault final : public multipass::VMImageVault { - multipass::VMImage fetch_image(const multipass::FetchType&, const multipass::Query&, const PrepareAction& prepare, - const multipass::ProgressMonitor&, const bool, - const std::optional&) override + multipass::VMImage fetch_image(const multipass::FetchType&, + const multipass::Query&, + const PrepareAction& prepare, + const multipass::ProgressMonitor&, + const bool, + const std::optional&, + const multipass::Path&) override { return prepare({dummy_image.name(), {}, {}, {}, {}, {}}); }; diff --git a/tests/test_alias_dict.cpp b/tests/test_alias_dict.cpp index 30bfb5883ec..189717cb588 100644 --- a/tests/test_alias_dict.cpp +++ b/tests/test_alias_dict.cpp @@ -26,7 +26,7 @@ #include "daemon_test_fixture.h" #include "fake_alias_config.h" #include "file_operations.h" -#include "json_utils.h" +#include "json_test_utils.h" #include "mock_file_ops.h" #include "mock_platform.h" #include "mock_settings.h" @@ -421,17 +421,6 @@ TEST_F(AliasDictionary, get_unexisting_alias_returns_nullopt) ASSERT_EQ(dict.get_alias("unexisting"), std::nullopt); } -TEST_F(AliasDictionary, creates_backup_db) -{ - populate_db_file(AliasesVector{{"some_alias", {"some_instance", "some_command", "map"}}}); - - QString bak_filename = QString::fromStdString(db_filename() + ".bak"); - ASSERT_FALSE(QFile::exists(bak_filename)); - - populate_db_file(AliasesVector{{"another_alias", {"an_instance", "a_command", "map"}}}); - ASSERT_TRUE(QFile::exists(bak_filename)); -} - TEST_F(AliasDictionary, throws_when_open_alias_file_fails) { auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); @@ -470,32 +459,56 @@ TEST_P(FormatterTestsuite, table) const std::string csv_head{"Alias,Instance,Command,Working directory,Context\n"}; -INSTANTIATE_TEST_SUITE_P( - AliasDictionary, FormatterTestsuite, - Values(std::make_tuple(AliasesVector{}, csv_head, - "{\n \"active-context\": \"default\",\n \"contexts\": {\n \"default\": {\n" - " }\n }\n}\n", - "No aliases defined.\n", "active_context: default\naliases:\n default: ~\n"), - std::make_tuple( - AliasesVector{{"lsp", {"primary", "ls", "map"}}, {"llp", {"primary", "ls", "map"}}}, - csv_head + "llp,primary,ls,map,default*\nlsp,primary,ls,map,default*\n", - "{\n \"active-context\": \"default\",\n \"contexts\": {\n" - " \"default\": {\n" - " \"llp\": {\n" - " \"command\": \"ls\",\n" - " \"instance\": \"primary\",\n" - " \"working-directory\": \"map\"\n" - " },\n" - " \"lsp\": {\n" - " \"command\": \"ls\",\n" - " \"instance\": \"primary\",\n" - " \"working-directory\": \"map\"\n" - " }\n }\n }\n}\n", - "Alias Instance Command Context Working directory\n" - "llp primary ls default* map\nlsp primary ls default* map\n", - "active_context: default\naliases:\n default:\n" - " - alias: llp\n command: ls\n instance: primary\n working-directory: map\n" - " - alias: lsp\n command: ls\n instance: primary\n working-directory: map\n"))); +INSTANTIATE_TEST_SUITE_P(AliasDictionary, + FormatterTestsuite, + Values(std::make_tuple(AliasesVector{}, + csv_head, + "{\n" + " \"active-context\": \"default\",\n" + " \"contexts\": {\n" + " \"default\": {\n" + " }\n" + " }\n" + "}\n", + "No aliases defined.\n", + "active_context: default\n" + "aliases:\n" + " default: ~\n"), + std::make_tuple(AliasesVector{{"lsp", {"primary", "ls", "map"}}, + {"llp", {"primary", "ls", "map"}}}, + csv_head + "llp,primary,ls,map,default*\n" + "lsp,primary,ls,map,default*\n", + "{\n" + " \"active-context\": \"default\",\n" + " \"contexts\": {\n" + " \"default\": {\n" + " \"llp\": {\n" + " \"command\": \"ls\",\n" + " \"instance\": \"primary\",\n" + " \"working-directory\": \"map\"\n" + " },\n" + " \"lsp\": {\n" + " \"command\": \"ls\",\n" + " \"instance\": \"primary\",\n" + " \"working-directory\": \"map\"\n" + " }\n" + " }\n" + " }\n" + "}\n", + "Alias Instance Command Context Working directory\n" + "llp primary ls default* map\n" + "lsp primary ls default* map\n", + "active_context: default\n" + "aliases:\n" + " default:\n" + " - alias: llp\n" + " command: ls\n" + " instance: primary\n" + " working-directory: map\n" + " - alias: lsp\n" + " command: ls\n" + " instance: primary\n" + " working-directory: map\n"))); struct RemoveInstanceTestsuite : public AliasDictionary, public WithParamInterface>> @@ -557,11 +570,14 @@ TEST_P(DaemonAliasTestsuite, purge_removes_purged_instance_aliases_and_scripts) auto mock_image_vault = std::make_unique>(); EXPECT_CALL(*mock_image_vault, remove(_)).WillRepeatedly(Return()); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillRepeatedly(Return(mp::VMImage{})); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillRepeatedly(Return(mp::VMImage{})); EXPECT_CALL(*mock_image_vault, prune_expired_images()).WillRepeatedly(Return()); EXPECT_CALL(*mock_image_vault, has_record_for(_)).WillRepeatedly(Return(true)); config_builder.vault = std::move(mock_image_vault); + auto mock_factory = use_a_mock_vm_factory(); + + EXPECT_CALL(*mock_factory, remove_resources_for(_)).WillRepeatedly(Return()); std::string json_contents = make_instance_json(std::nullopt, {}, {"primary"}); diff --git a/tests/test_base_virtual_machine.cpp b/tests/test_base_virtual_machine.cpp index aa49695ea3d..06e5907911d 100644 --- a/tests/test_base_virtual_machine.cpp +++ b/tests/test_base_virtual_machine.cpp @@ -18,6 +18,7 @@ #include "common.h" #include "dummy_ssh_key_provider.h" #include "mock_ssh_test_fixture.h" +#include "temp_dir.h" #include @@ -33,10 +34,14 @@ namespace { struct StubBaseVirtualMachine : public mp::BaseVirtualMachine { - StubBaseVirtualMachine(const mp::VirtualMachine::State s = mp::VirtualMachine::State::off) - : mp::BaseVirtualMachine("stub") + StubBaseVirtualMachine(mp::VirtualMachine::State s = mp::VirtualMachine::State::off) + : StubBaseVirtualMachine{s, std::make_unique()} + { + } + + StubBaseVirtualMachine(mp::VirtualMachine::State s, std::unique_ptr&& tmp_dir) + : mp::BaseVirtualMachine{s, "stub", tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} { - state = s; } void stop() override @@ -69,7 +74,7 @@ struct StubBaseVirtualMachine : public mp::BaseVirtualMachine return 42; } - std::string ssh_hostname(std::chrono::milliseconds timeout) override + std::string ssh_hostname(std::chrono::milliseconds /*timeout*/) override { return "localhost"; } @@ -89,7 +94,7 @@ struct StubBaseVirtualMachine : public mp::BaseVirtualMachine return ""; } - void wait_until_ssh_up(std::chrono::milliseconds timeout, const mp::SSHKeyProvider& key_provider) override + void wait_until_ssh_up(std::chrono::milliseconds /*timeout*/, const mp::SSHKeyProvider& /*key_provider*/) override { } @@ -112,6 +117,22 @@ struct StubBaseVirtualMachine : public mp::BaseVirtualMachine void resize_disk(const mp::MemorySize&) override { } + +protected: + std::shared_ptr make_specific_snapshot(const std::string& /*snapshot_name*/, + const std::string& /*comment*/, + const mp::VMSpecs& /*specs*/, + std::shared_ptr /*parent*/) override + { + return nullptr; + } + + virtual std::shared_ptr make_specific_snapshot(const QString& /*json*/) override + { + return nullptr; + } + + std::unique_ptr&& tmp_dir; }; struct BaseVM : public Test diff --git a/tests/test_base_virtual_machine_factory.cpp b/tests/test_base_virtual_machine_factory.cpp index c0a2fe53985..25a9c624a57 100644 --- a/tests/test_base_virtual_machine_factory.cpp +++ b/tests/test_base_virtual_machine_factory.cpp @@ -37,13 +37,21 @@ namespace { struct MockBaseFactory : mp::BaseVirtualMachineFactory { + MockBaseFactory() : MockBaseFactory{std::make_unique()} + { + } + + MockBaseFactory(std::unique_ptr&& tmp_dir) + : mp::BaseVirtualMachineFactory{tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} + { + } + MOCK_METHOD(mp::VirtualMachine::UPtr, create_virtual_machine, (const mp::VirtualMachineDescription&, mp::VMStatusMonitor&), (override)); - MOCK_METHOD(void, remove_resources_for, (const std::string&), (override)); MOCK_METHOD(mp::VMImage, prepare_source_image, (const mp::VMImage&), (override)); MOCK_METHOD(void, prepare_instance_image, (const mp::VMImage&, const mp::VirtualMachineDescription&), (override)); MOCK_METHOD(void, hypervisor_health_check, (), (override)); - MOCK_METHOD(QString, get_backend_version_string, (), (override)); + MOCK_METHOD(QString, get_backend_version_string, (), (const, override)); MOCK_METHOD(void, prepare_networking, (std::vector&), (override)); MOCK_METHOD(std::vector, networks, (), (const, override)); MOCK_METHOD(std::string, create_bridge_with, (const mp::NetworkInterfaceInfo&), (override)); @@ -51,6 +59,7 @@ struct MockBaseFactory : mp::BaseVirtualMachineFactory (mp::NetworkInterface & net, std::vector& host_nets, const std::string& bridge_type), (override)); + MOCK_METHOD(void, remove_resources_for_impl, (const std::string&), (override)); std::string base_create_bridge_with(const mp::NetworkInterfaceInfo& interface) { @@ -68,6 +77,8 @@ struct MockBaseFactory : mp::BaseVirtualMachineFactory { return mp::BaseVirtualMachineFactory::prepare_interface(net, host_nets, bridge_type); // protected } + + std::unique_ptr tmp_dir; }; struct BaseFactory : public Test diff --git a/tests/test_cli_client.cpp b/tests/test_cli_client.cpp index 91f47429ebb..36a70dd7eee 100644 --- a/tests/test_cli_client.cpp +++ b/tests/test_cli_client.cpp @@ -136,6 +136,16 @@ struct MockDaemonRpc : public mp::DaemonRpc (grpc::ServerContext * context, (grpc::ServerReaderWriter * server)), (override)); + MOCK_METHOD(grpc::Status, + snapshot, + (grpc::ServerContext * context, + (grpc::ServerReaderWriter * server)), + (override)); + MOCK_METHOD(grpc::Status, + restore, + (grpc::ServerContext * context, + (grpc::ServerReaderWriter * server)), + (override)); }; struct Client : public Test @@ -325,7 +335,7 @@ struct Client : public Test for (mp::InstanceStatus_Status status : statuses) { - auto list_entry = list_reply.add_instances(); + auto list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->mutable_instance_status()->set_status(status); } @@ -342,14 +352,17 @@ struct Client : public Test } template - auto check_request_and_return(const Matcher& matcher, const grpc::Status& status) + auto check_request_and_return(const Matcher& matcher, + const grpc::Status& status, + const ReplyType& reply = ReplyType{}) { - return [&matcher, &status](grpc::ServerReaderWriter* server) { + return [&matcher, &status, reply = std::move(reply)](grpc::ServerReaderWriter* server) { RequestType request; server->Read(&request); EXPECT_THAT(request, matcher); + server->Write(std::move(reply)); return status; }; } @@ -396,9 +409,9 @@ auto make_info_function(const std::string& source_path = "", const std::string& mp::InfoReply info_reply; - if (request.instance_names().instance_name(0) == "primary") + if (request.instance_snapshot_pairs(0).instance_name() == "primary") { - auto vm_info = info_reply.add_info(); + auto vm_info = info_reply.add_details(); vm_info->set_name("primary"); vm_info->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); @@ -1018,11 +1031,11 @@ TEST_F(Client, launch_cmd_memory_fails_duplicate_options) TEST_F(Client, launch_cmd_memory_deprecated_option_warning) { - std::stringstream cout_stream; + std::stringstream cerr_stream; EXPECT_CALL(mock_daemon, launch(_, _)); - EXPECT_THAT(send_command({"launch", "--mem", "2048M"}, cout_stream, trash_stream), Eq(mp::ReturnCode::Ok)); - EXPECT_NE(std::string::npos, cout_stream.str().find("warning: \"--mem\"")) << "cout has: " << cout_stream.str(); + EXPECT_THAT(send_command({"launch", "--mem", "2048M"}, trash_stream, cerr_stream), Eq(mp::ReturnCode::Ok)); + EXPECT_NE(std::string::npos, cerr_stream.str().find("Warning: the \"--mem\"")) << "cout has: " << cerr_stream.str(); } TEST_F(Client, launch_cmd_cpu_option_ok) @@ -1705,9 +1718,10 @@ TEST_F(Client, help_cmd_help_ok) } // info cli tests -TEST_F(Client, info_cmd_fails_no_args) +TEST_F(Client, infoCmdOkNoArgs) { - EXPECT_THAT(send_command({"info"}), Eq(mp::ReturnCode::CommandLineError)); + EXPECT_CALL(mock_daemon, info(_, _)); + EXPECT_THAT(send_command({"info"}), Eq(mp::ReturnCode::Ok)); } TEST_F(Client, info_cmd_ok_with_one_arg) @@ -1769,9 +1783,11 @@ TEST_F(Client, infoCmdSucceedsWithAllAndNoRuntimeInformation) TEST_F(Client, list_cmd_ok_no_args) { const auto list_matcher = Property(&mp::ListRequest::request_ipv4, IsTrue()); + mp::ListReply reply; + reply.mutable_instance_list(); EXPECT_CALL(mock_daemon, list) - .WillOnce(WithArg<1>(check_request_and_return(list_matcher, ok))); + .WillOnce(WithArg<1>(check_request_and_return(list_matcher, ok, reply))); EXPECT_THAT(send_command({"list"}), Eq(mp::ReturnCode::Ok)); } @@ -1788,12 +1804,19 @@ TEST_F(Client, list_cmd_help_ok) TEST_F(Client, list_cmd_no_ipv4_ok) { const auto list_matcher = Property(&mp::ListRequest::request_ipv4, IsFalse()); + mp::ListReply reply; + reply.mutable_instance_list(); EXPECT_CALL(mock_daemon, list) - .WillOnce(WithArg<1>(check_request_and_return(list_matcher, ok))); + .WillOnce(WithArg<1>(check_request_and_return(list_matcher, ok, reply))); EXPECT_THAT(send_command({"list", "--no-ipv4"}), Eq(mp::ReturnCode::Ok)); } +TEST_F(Client, listCmdFailsWithIpv4AndSnapshots) +{ + EXPECT_THAT(send_command({"list", "--no-ipv4", "--snapshots"}), Eq(mp::ReturnCode::CommandLineError)); +} + // mount cli tests // Note: mpt::test_data_path() returns an absolute path TEST_F(Client, mount_cmd_good_absolute_source_path) @@ -3135,6 +3158,152 @@ TEST_F(Client, help_cmd_launch_same_launch_cmd_help) EXPECT_THAT(help_cmd_launch.str(), Eq(launch_cmd_help.str())); } +// snapshot cli tests +TEST_F(Client, snapshotCmdHelpOk) +{ + EXPECT_EQ(send_command({"snapshot", "--help"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, snapshotCmdNoOptionsOk) +{ + EXPECT_CALL(mock_daemon, snapshot); + EXPECT_EQ(send_command({"snapshot", "foo"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, snapshotCmdNameAlternativesOk) +{ + EXPECT_CALL(mock_daemon, snapshot).Times(2); + EXPECT_EQ(send_command({"snapshot", "-n", "bar", "foo"}), mp::ReturnCode::Ok); + EXPECT_EQ(send_command({"snapshot", "--name", "bar", "foo"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, snapshotCmdNameConsumesArg) +{ + EXPECT_CALL(mock_daemon, snapshot).Times(0); + EXPECT_EQ(send_command({"snapshot", "--name", "foo"}), mp::ReturnCode::CommandLineError); + EXPECT_EQ(send_command({"snapshot", "-n", "foo"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, snapshotCmdCommentOptionAlternativesOk) +{ + EXPECT_CALL(mock_daemon, snapshot).Times(3); + EXPECT_EQ(send_command({"snapshot", "--comment", "a comment", "foo"}), mp::ReturnCode::Ok); + EXPECT_EQ(send_command({"snapshot", "-c", "a comment", "foo"}), mp::ReturnCode::Ok); + EXPECT_EQ(send_command({"snapshot", "-m", "a comment", "foo"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, snapshotCmdCommentConsumesArg) +{ + EXPECT_CALL(mock_daemon, snapshot).Times(0); + EXPECT_EQ(send_command({"snapshot", "--comment", "foo"}), mp::ReturnCode::CommandLineError); + EXPECT_EQ(send_command({"snapshot", "-c", "foo"}), mp::ReturnCode::CommandLineError); + EXPECT_EQ(send_command({"snapshot", "-m", "foo"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, snapshotCmdTooFewArgsFails) +{ + EXPECT_EQ(send_command({"snapshot", "-m", "Who controls the past controls the future"}), + mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, snapshotCmdTooManyArgsFails) +{ + EXPECT_EQ(send_command({"snapshot", "foo", "bar"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, snapshotCmdInvalidOptionFails) +{ + EXPECT_EQ(send_command({"snapshot", "--snap"}), mp::ReturnCode::CommandLineError); +} + +// restore cli tests +TEST_F(Client, restoreCmdHelpOk) +{ + EXPECT_EQ(send_command({"restore", "--help"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, restoreCmdGoodArgsOk) +{ + EXPECT_CALL(mock_daemon, restore); + EXPECT_EQ(send_command({"restore", "foo.snapshot1", "--destructive"}), mp::ReturnCode::Ok); +} + +TEST_F(Client, restoreCmdTooFewArgsFails) +{ + EXPECT_EQ(send_command({"restore", "--destructive"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, restoreCmdTooManyArgsFails) +{ + EXPECT_EQ(send_command({"restore", "foo.snapshot1", "bar.snapshot2"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, restoreCmdMissingInstanceFails) +{ + EXPECT_EQ(send_command({"restore", ".snapshot1"}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, restoreCmdMissingSnapshotFails) +{ + EXPECT_EQ(send_command({"restore", "foo."}), mp::ReturnCode::CommandLineError); +} + +TEST_F(Client, restoreCmdInvalidOptionFails) +{ + EXPECT_EQ(send_command({"restore", "--foo"}), mp::ReturnCode::CommandLineError); +} + +struct RestoreCommandClient : public Client +{ + RestoreCommandClient() + { + EXPECT_CALL(mock_terminal, cout).WillRepeatedly(ReturnRef(cout)); + EXPECT_CALL(mock_terminal, cerr).WillRepeatedly(ReturnRef(cerr)); + EXPECT_CALL(mock_terminal, cin).WillRepeatedly(ReturnRef(cin)); + } + + std::ostringstream cerr, cout; + std::istringstream cin; + mpt::MockTerminal mock_terminal; +}; + +TEST_F(RestoreCommandClient, restoreCmdConfirmsDesruction) +{ + cin.str("invalid input\nyes\n"); + + EXPECT_CALL(mock_terminal, cin_is_live()).WillOnce(Return(true)); + EXPECT_CALL(mock_terminal, cout_is_live()).WillOnce(Return(true)); + + EXPECT_CALL(mock_daemon, restore(_, _)).WillOnce([](auto, auto* server) { + mp::RestoreRequest request; + server->Read(&request); + + EXPECT_FALSE(request.destructive()); + + mp::RestoreReply reply; + reply.set_confirm_destructive(true); + server->Write(reply); + return grpc::Status{}; + }); + + EXPECT_EQ(setup_client_and_run({"restore", "foo.snapshot1"}, mock_terminal), mp::ReturnCode::Ok); + EXPECT_TRUE(cout.str().find("Please answer yes/no")); +} + +TEST_F(RestoreCommandClient, restoreCmdNotDestructiveNotLiveTermFails) +{ + EXPECT_CALL(mock_terminal, cin_is_live()).WillOnce(Return(false)); + + EXPECT_CALL(mock_daemon, restore(_, _)).WillOnce([](auto, auto* server) { + mp::RestoreReply reply; + reply.set_confirm_destructive(true); + server->Write(reply); + return grpc::Status{}; + }); + + EXPECT_THROW(setup_client_and_run({"restore", "foo.snapshot1"}, mock_terminal), std::runtime_error); +} + // authenticate cli tests TEST_F(Client, authenticateCmdGoodPassphraseOk) { @@ -3738,64 +3907,18 @@ TEST_F(ClientAlias, unaliasDashDashAllClashesWithOtherArguments) "another_alias,another_instance,another_command,default,default*\n"); } -TEST_F(ClientAlias, fails_when_remove_backup_alias_file_fails) -{ - auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - - EXPECT_CALL(*mock_file_ops, exists(A())) - .WillOnce(Return(false)) - .WillOnce(Return(true)) - .WillOnce(Return(true)); - EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)); // mpu::create_temp_file_with_path() - EXPECT_CALL(*mock_file_ops, open(_, _)).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_file_ops, write(_, _)).WillOnce(Return(true)); - EXPECT_CALL(*mock_file_ops, remove(_)).WillOnce(Return(false)); - - EXPECT_CALL(mock_daemon, info(_, _)).Times(AtMost(1)).WillRepeatedly(make_info_function()); - - std::stringstream cerr_stream; - send_command({"alias", "primary:command", "alias_name"}, trash_stream, cerr_stream); - - ASSERT_THAT(cerr_stream.str(), HasSubstr("cannot remove old aliases backup file ")); -} - -TEST_F(ClientAlias, fails_renaming_alias_file_fails) -{ - auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - - EXPECT_CALL(*mock_file_ops, exists(A())) - .WillOnce(Return(false)) - .WillOnce(Return(true)) - .WillOnce(Return(false)); - EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)); // mpu::create_temp_file_with_path() - EXPECT_CALL(*mock_file_ops, open(_, _)).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_file_ops, write(_, _)).WillOnce(Return(true)); - EXPECT_CALL(*mock_file_ops, rename(_, _)).WillOnce(Return(false)); - - EXPECT_CALL(mock_daemon, info(_, _)).Times(AtMost(1)).WillRepeatedly(make_info_function()); - - std::stringstream cerr_stream; - send_command({"alias", "primary:command", "alias_name"}, trash_stream, cerr_stream); - - ASSERT_THAT(cerr_stream.str(), HasSubstr("cannot rename aliases config to ")); -} - -TEST_F(ClientAlias, fails_creating_alias_file_fails) +TEST_F(ClientAlias, fails_if_unable_to_create_directory) { auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, exists(A())).WillOnce(Return(false)).WillOnce(Return(false)); - EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)); // mpu::create_temp_file_with_path() - EXPECT_CALL(*mock_file_ops, open(_, _)).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_file_ops, write(_, _)).WillOnce(Return(true)); - EXPECT_CALL(*mock_file_ops, rename(_, _)).WillOnce(Return(false)); - + EXPECT_CALL(*mock_file_ops, exists(A())).WillOnce(Return(false)); + EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(false)); EXPECT_CALL(mock_daemon, info(_, _)).Times(AtMost(1)).WillRepeatedly(make_info_function()); std::stringstream cerr_stream; send_command({"alias", "primary:command", "alias_name"}, trash_stream, cerr_stream); - ASSERT_THAT(cerr_stream.str(), HasSubstr("cannot create aliases config file ")); + ASSERT_THAT(cerr_stream.str(), HasSubstr("Could not create path")); } TEST_F(ClientAlias, creating_first_alias_displays_message) diff --git a/tests/test_daemon.cpp b/tests/test_daemon.cpp index 0d6c1267082..f703dd5663b 100644 --- a/tests/test_daemon.cpp +++ b/tests/test_daemon.cpp @@ -20,11 +20,12 @@ #include "daemon_test_fixture.h" #include "dummy_ssh_key_provider.h" #include "fake_alias_config.h" -#include "json_utils.h" +#include "json_test_utils.h" #include "mock_daemon.h" #include "mock_environment_helpers.h" #include "mock_file_ops.h" #include "mock_image_host.h" +#include "mock_json_utils.h" #include "mock_logger.h" #include "mock_platform.h" #include "mock_server_reader_writer.h" @@ -159,8 +160,15 @@ TEST_F(Daemon, receives_commands_and_calls_corresponding_slot) .WillOnce(Invoke(&daemon, &mpt::MockDaemon::set_promise_value)); EXPECT_CALL(daemon, info(_, _, _)) .WillOnce(Invoke(&daemon, &mpt::MockDaemon::set_promise_value)); - EXPECT_CALL(daemon, list(_, _, _)) - .WillOnce(Invoke(&daemon, &mpt::MockDaemon::set_promise_value)); + EXPECT_CALL(daemon, list(_, _, _)).WillOnce([](auto, auto server, auto status_promise) { + mp::ListReply reply; + reply.mutable_instance_list(); + + server->Write(reply); + status_promise->set_value(grpc::Status::OK); + + return grpc::Status{}; + }); EXPECT_CALL(daemon, recover(_, _, _)) .WillOnce(Invoke(&daemon, &mpt::MockDaemon::set_promise_value)); EXPECT_CALL(daemon, start(_, _, _)) @@ -668,7 +676,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundPassesExpectedAliases) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); auto alias = std::make_optional(std::make_pair(alias_name, mp::AliasDefinition{name, alias_command, alias_wdir})); @@ -707,7 +715,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundMountsWorkspace) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -741,7 +749,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundMountsWorkspaceConfined) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -779,7 +787,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundMountsWorkspaceInExisting EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -822,7 +830,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundDoesNotMountUnwrittableWo EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -836,7 +844,10 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundDoesNotMountUnwrittableWo .WillOnce(Return(temp_dir.path())); auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(false)); + EXPECT_CALL(*mock_file_ops, open(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_file_ops, write(_, _)).WillRepeatedly(Return(1234)); + EXPECT_CALL(*mock_file_ops, commit(_)).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)).WillOnce(Return(true)).WillOnce(Return(false)); config_builder.blueprint_provider = std::move(mock_blueprint_provider); config_builder.vault = std::move(mock_image_vault); @@ -866,7 +877,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundButCannotMount) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -880,7 +891,10 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundButCannotMount) .WillOnce(Return(temp_dir.path())); auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)); + EXPECT_CALL(*mock_file_ops, open(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_file_ops, write(_, _)).WillRepeatedly(Return(1234)); + EXPECT_CALL(*mock_file_ops, commit(_)).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_file_ops, mkpath(_, _)).WillOnce(Return(true)).WillOnce(Return(true)).WillOnce(Return(true)); config_builder.blueprint_provider = std::move(mock_blueprint_provider); config_builder.vault = std::move(mock_image_vault); @@ -914,7 +928,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundPassesExpectedAliasesWith EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, command_line_name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); auto alias = std::make_optional(std::make_pair(alias_name, mp::AliasDefinition{name, alias_command, alias_wdir})); @@ -961,7 +975,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundDoesNotOverwriteAliases) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); auto alias = std::make_optional(std::make_pair(alias_name, mp::AliasDefinition{name, alias_command, alias_wdir})); @@ -1010,7 +1024,7 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundDoesNotOverwriteAliasesIf EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); auto alias = std::make_optional(std::make_pair(alias_name, mp::AliasDefinition{name, alias_command, alias_wdir})); @@ -1023,10 +1037,10 @@ TEST_F(DaemonCreateLaunchAliasTestSuite, blueprintFoundDoesNotOverwriteAliasesIf config_builder.vault = std::move(mock_image_vault); mp::Daemon daemon{config_builder.build()}; - std::stringstream cout_stream; - send_command({"launch", name}, cout_stream); + std::stringstream cout_stream, cerr_stream; + send_command({"launch", name}, cout_stream, cerr_stream); - EXPECT_THAT(cout_stream.str(), HasSubstr("Warning: unable to create alias " + alias_name)); + EXPECT_THAT(cerr_stream.str(), HasSubstr("Warning: unable to create alias " + alias_name)); cout_stream.str(""); send_command({"aliases", "--format=csv"}, cout_stream); @@ -1051,7 +1065,7 @@ TEST_P(DaemonCreateLaunchTestSuite, blueprint_found_passes_expected_data) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce(mpt::fetch_blueprint_for_lambda(num_cores, mem_size, disk_space, release, remote)); @@ -1079,7 +1093,7 @@ TEST_P(DaemonCreateLaunchTestSuite, blueprint_not_found_passes_expected_data) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, empty)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, empty)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, empty)); config_builder.vault = std::move(mock_image_vault); mp::Daemon daemon{config_builder.build()}; @@ -1303,6 +1317,9 @@ TEST_P(LaunchStorageCheckSuite, launch_fails_with_invalid_data_directory) config_builder.data_directory = QString("invalid_data_directory"); mp::Daemon daemon{config_builder.build()}; + auto [mock_json_utils, guard] = mpt::MockJsonUtils::inject(); + EXPECT_CALL(*mock_json_utils, write_json).Times(1); // avoid creating directory + std::stringstream stream; EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)).Times(0); send_command({GetParam()}, trash_stream, stream); @@ -1334,7 +1351,7 @@ TEST_F(DaemonCreateLaunchTestSuite, blueprintFromFileCallsCorrectFunction) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)).Times(1); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).Times(1); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)).Times(0); @@ -1399,6 +1416,10 @@ TEST_F(Daemon, reads_mac_addresses_from_json) const auto [temp_dir, filename] = plant_instance_json(fake_json_contents(mac_addr, extra_interfaces)); + EXPECT_CALL(*use_a_mock_vm_factory(), create_virtual_machine).WillRepeatedly(WithArg<0>([](const auto& desc) { + return std::make_unique(desc.vm_name); + })); + // Make the daemon look for the JSON on our temporary directory. It will read the contents of the file. config_builder.data_directory = temp_dir->path(); mp::Daemon daemon{config_builder.build()}; @@ -1406,12 +1427,13 @@ TEST_F(Daemon, reads_mac_addresses_from_json) // Check that the instance was indeed read and there were no errors. { StrictMock> mock_server; + mp::ListReply list_reply; - auto instance_matcher = Property(&mp::ListVMInstance::name, "real-zebraphant"); - EXPECT_CALL(mock_server, Write(Property(&mp::ListReply::instances, ElementsAre(instance_matcher)), _)) - .WillOnce(Return(true)); + auto instance_matcher = UnorderedElementsAre(Property(&mp::ListVMInstance::name, "real-zebraphant")); + EXPECT_CALL(mock_server, Write(_, _)).WillOnce(DoAll(SaveArg<0>(&list_reply), Return(true))); EXPECT_TRUE(call_daemon_slot(daemon, &mp::Daemon::list, mp::ListRequest{}, mock_server).ok()); + EXPECT_THAT(list_reply.instance_list().instances(), instance_matcher); } // Removing the JSON is possible now because data was already read. This step is not necessary, but doing it we @@ -1461,6 +1483,10 @@ TEST_F(Daemon, writesAndReadsMountsInJson) const auto [temp_dir, filename] = plant_instance_json(fake_json_contents(mac_addr, extra_interfaces, mounts)); + EXPECT_CALL(*use_a_mock_vm_factory(), create_virtual_machine).WillRepeatedly(WithArg<0>([](const auto& desc) { + return std::make_unique(desc.vm_name); + })); + // Make the daemon look for the JSON on our temporary directory. It will read the contents of the file. config_builder.data_directory = temp_dir->path(); mp::Daemon daemon{config_builder.build()}; @@ -1468,12 +1494,13 @@ TEST_F(Daemon, writesAndReadsMountsInJson) // Check that the instance was indeed read and there were no errors. { StrictMock> mock_server; + mp::ListReply list_reply; - auto instance_matcher = Property(&mp::ListVMInstance::name, "real-zebraphant"); - EXPECT_CALL(mock_server, Write(Property(&mp::ListReply::instances, ElementsAre(instance_matcher)), _)) - .WillOnce(Return(true)); + auto instance_matcher = UnorderedElementsAre(Property(&mp::ListVMInstance::name, "real-zebraphant")); + EXPECT_CALL(mock_server, Write(_, _)).WillOnce(DoAll(SaveArg<0>(&list_reply), Return(true))); EXPECT_TRUE(call_daemon_slot(daemon, &mp::Daemon::list, mp::ListRequest{}, mock_server).ok()); + EXPECT_THAT(list_reply.instance_list().instances(), instance_matcher); } QFile::remove(filename); // Remove the JSON. @@ -1495,6 +1522,10 @@ TEST_F(Daemon, writes_and_reads_ordered_maps_in_json) const auto [temp_dir, filename] = plant_instance_json(fake_json_contents("52:54:00:73:76:29", std::vector{}, mounts)); + EXPECT_CALL(*use_a_mock_vm_factory(), create_virtual_machine).WillRepeatedly(WithArg<0>([](const auto& desc) { + return std::make_unique(desc.vm_name); + })); + config_builder.data_directory = temp_dir->path(); mp::Daemon daemon{config_builder.build()}; @@ -1684,27 +1715,31 @@ TEST_F(Daemon, ctor_drops_removed_instances) config_builder.data_directory = temp_dir->path(); auto mock_image_vault = std::make_unique>(); - EXPECT_CALL(*mock_image_vault, fetch_image(_, Field(&mp::Query::name, stayed), _, _, _, _)) + EXPECT_CALL(*mock_image_vault, fetch_image(_, Field(&mp::Query::name, stayed), _, _, _, _, _)) .WillRepeatedly(DoDefault()); // returns an image that can be verified to exist for this instance - EXPECT_CALL(*mock_image_vault, fetch_image(_, Field(&mp::Query::name, gone), _, _, _, _)) + EXPECT_CALL(*mock_image_vault, fetch_image(_, Field(&mp::Query::name, gone), _, _, _, _, _)) .WillOnce(Return(mp::VMImage{"/path/to/nowhere", "", "", "", "", {}})); // an image that can't be verified to // exist for this instance config_builder.vault = std::move(mock_image_vault); auto mock_factory = use_a_mock_vm_factory(); EXPECT_CALL(*mock_factory, create_virtual_machine(Field(&mp::VirtualMachineDescription::vm_name, stayed), _)) - .Times(1); + .Times(1) + .WillRepeatedly( + WithArg<0>([](const auto& desc) { return std::make_unique(desc.vm_name); })); EXPECT_CALL(*mock_factory, create_virtual_machine(Field(&mp::VirtualMachineDescription::vm_name, gone), _)) .Times(0); mp::Daemon daemon{config_builder.build()}; StrictMock> mock_server; - auto stayed_matcher = Property(&mp::ListVMInstance::name, stayed); - EXPECT_CALL(mock_server, Write(Property(&mp::ListReply::instances, ElementsAre(stayed_matcher)), _)) - .WillOnce(Return(true)); + mp::ListReply list_reply; + + auto stayed_matcher = UnorderedElementsAre(Property(&mp::ListVMInstance::name, stayed)); + EXPECT_CALL(mock_server, Write(_, _)).WillOnce(DoAll(SaveArg<0>(&list_reply), Return(true))); EXPECT_TRUE(call_daemon_slot(daemon, &mp::Daemon::list, mp::ListRequest{}, mock_server).ok()); + EXPECT_THAT(list_reply.instance_list().instances(), stayed_matcher); auto updated_json = mpt::load(filename); EXPECT_THAT(updated_json.toStdString(), AllOf(HasSubstr(stayed), Not(HasSubstr(gone)))); @@ -2175,14 +2210,13 @@ TEST_F(Daemon, info_all_returns_all_instances) return std::make_unique(desc.vm_name); })); - const auto names_matcher = UnorderedElementsAre(Property(&mp::InfoReply::Info::name, good_instance_name), - Property(&mp::InfoReply::Info::name, deleted_instance_name)); + const auto names_matcher = UnorderedElementsAre(Property(&mp::DetailedInfoItem::name, good_instance_name), + Property(&mp::DetailedInfoItem::name, deleted_instance_name)); StrictMock> mock_server{}; - EXPECT_CALL(mock_server, Write(Property(&mp::InfoReply::info, names_matcher), _)).WillOnce(Return(true)); + EXPECT_CALL(mock_server, Write(Property(&mp::InfoReply::details, names_matcher), _)).WillOnce(Return(true)); mp::Daemon daemon{config_builder.build()}; - call_daemon_slot(daemon, &mp::Daemon::info, mp::InfoRequest{}, mock_server); } } // namespace diff --git a/tests/test_daemon_launch.cpp b/tests/test_daemon_launch.cpp index 8db790c38e5..5b3740e4fe9 100644 --- a/tests/test_daemon_launch.cpp +++ b/tests/test_daemon_launch.cpp @@ -72,7 +72,7 @@ TEST_F(TestDaemonLaunch, blueprintFoundMountsWorkspaceWithNameOverride) EXPECT_CALL(*mock_factory, create_virtual_machine(_, _)) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, command_line_name)); - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)).WillOnce(mpt::fetch_image_lambda(release, remote)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) .WillOnce( @@ -127,7 +127,7 @@ TEST_F(TestDaemonLaunch, v2BlueprintFoundPropagatesSha) .WillOnce(mpt::create_virtual_machine_lambda(num_cores, mem_size, disk_space, command_line_name)); // The expectation of this test is set in fetch_image_lambda(). - EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _)) + EXPECT_CALL(*mock_image_vault, fetch_image(_, _, _, _, _, _, _)) .WillOnce(mpt::fetch_image_lambda(release, remote, true)); EXPECT_CALL(*mock_blueprint_provider, fetch_blueprint_for(_, _, _)) diff --git a/tests/test_format_utils.cpp b/tests/test_format_utils.cpp index 247ea835e52..80252d380ab 100644 --- a/tests/test_format_utils.cpp +++ b/tests/test_format_utils.cpp @@ -210,7 +210,8 @@ TEST(StaticFormatFunctions, columnWidthOnEmptyInputWorks) const auto get_width = [](const auto& str) -> int { return str.length(); }; int min_w = 3; - ASSERT_EQ(mp::format::column_width(empty_vector.begin(), empty_vector.end(), get_width, min_w), min_w); + ASSERT_EQ(mp::format::column_width(empty_vector.begin(), empty_vector.end(), get_width, 0, min_w), + mp::format::col_buffer); } TEST(StaticFormatFunctions, columnWidthOnWideInputWorks) @@ -222,16 +223,16 @@ TEST(StaticFormatFunctions, columnWidthOnWideInputWorks) int min_w = 3; int space = 1; - ASSERT_EQ(mp::format::column_width(str_vector.begin(), str_vector.end(), get_width, min_w, space), - wider_str.length() + space); + ASSERT_EQ(mp::format::column_width(str_vector.begin(), str_vector.end(), get_width, space, min_w), + wider_str.length() + mp::format::col_buffer); } TEST(StaticFormatFunctions, columnWidthOnNarrowInputWorks) { const auto str_vector = std::vector{"n", "n2"}; const auto get_width = [](const auto& str) -> int { return str.length(); }; - int min_w = 3; + int min_w = 7; int space = 2; - ASSERT_EQ(mp::format::column_width(str_vector.begin(), str_vector.end(), get_width, min_w, space), 4); + ASSERT_EQ(mp::format::column_width(str_vector.begin(), str_vector.end(), get_width, space, min_w), min_w); } diff --git a/tests/test_image_vault.cpp b/tests/test_image_vault.cpp index 26a417e77d1..73ecc923cd8 100644 --- a/tests/test_image_vault.cpp +++ b/tests/test_image_vault.cpp @@ -19,6 +19,7 @@ #include "disabling_macros.h" #include "file_operations.h" #include "mock_image_host.h" +#include "mock_json_utils.h" #include "mock_logger.h" #include "mock_process_factory.h" #include "path.h" @@ -163,12 +164,16 @@ struct ImageVault : public testing::Test mpt::TrackingURLDownloader url_downloader; std::vector hosts; NiceMock host; + mpt::MockJsonUtils::GuardedMock mock_json_utils_injection = mpt::MockJsonUtils::inject(); + mpt::MockJsonUtils& mock_json_utils = *mock_json_utils_injection.first; mp::ProgressMonitor stub_monitor{[](int, int) { return true; }}; mp::VMImageVault::PrepareAction stub_prepare{ [](const mp::VMImage& source_image) -> mp::VMImage { return source_image; }}; mpt::TempDir cache_dir; mpt::TempDir data_dir; + mpt::TempDir save_dir; std::string instance_name{"valley-pied-piper"}; + QString instance_dir = save_dir.filePath(QString::fromStdString(instance_name)); mp::Query default_query{instance_name, "xenial", false, "", mp::Query::Type::Alias}; }; } // namespace @@ -176,8 +181,13 @@ struct ImageVault : public testing::Test TEST_F(ImageVault, downloads_image) { mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_TRUE(url_downloader.downloaded_urls.contains(host.image.url())); @@ -186,8 +196,13 @@ TEST_F(ImageVault, downloads_image) TEST_F(ImageVault, returned_image_contains_instance_name) { mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_TRUE(vm_image.image_path.contains(QString::fromStdString(instance_name))); } @@ -201,8 +216,13 @@ TEST_F(ImageVault, calls_prepare) prepare_called = true; return source_image; }; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_TRUE(prepare_called); } @@ -215,10 +235,20 @@ TEST_F(ImageVault, records_instanced_images) ++prepare_called_count; return source_image; }; - auto vm_image1 = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); - auto vm_image2 = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image1 = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); + auto vm_image2 = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_THAT(prepare_called_count, Eq(1)); @@ -234,13 +264,23 @@ TEST_F(ImageVault, caches_prepared_images) ++prepare_called_count; return source_image; }; - auto vm_image1 = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image1 = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); auto another_query = default_query; another_query.name = "valley-pied-piper-chat"; - auto vm_image2 = - vault.fetch_image(mp::FetchType::ImageOnly, another_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image2 = vault.fetch_image(mp::FetchType::ImageOnly, + another_query, + prepare, + stub_monitor, + false, + std::nullopt, + save_dir.filePath(QString::fromStdString(another_query.name))); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_THAT(prepare_called_count, Eq(1)); @@ -257,13 +297,27 @@ TEST_F(ImageVault, remembers_instance_images) return source_image; }; + EXPECT_CALL(mock_json_utils, write_json).WillRepeatedly([this](auto&&... args) { + return mock_json_utils.JsonUtils::write_json(std::forward(args)...); // call the real thing + }); + mp::DefaultVMImageVault first_vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image1 = - first_vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image1 = first_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); mp::DefaultVMImageVault another_vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image2 = - another_vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image2 = another_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_THAT(prepare_called_count, Eq(1)); @@ -278,15 +332,29 @@ TEST_F(ImageVault, remembers_prepared_images) return source_image; }; + EXPECT_CALL(mock_json_utils, write_json).WillRepeatedly([this](auto&&... args) { + return mock_json_utils.JsonUtils::write_json(std::forward(args)...); // call the real thing + }); + mp::DefaultVMImageVault first_vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image1 = - first_vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image1 = first_vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); auto another_query = default_query; another_query.name = "valley-pied-piper-chat"; mp::DefaultVMImageVault another_vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image2 = - another_vault.fetch_image(mp::FetchType::ImageOnly, another_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image2 = another_vault.fetch_image(mp::FetchType::ImageOnly, + another_query, + prepare, + stub_monitor, + false, + std::nullopt, + save_dir.filePath(QString::fromStdString(another_query.name))); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_THAT(prepare_called_count, Eq(1)); @@ -307,8 +375,13 @@ TEST_F(ImageVault, uses_image_from_prepare) }; mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); const auto image_data = mp::utils::contents_of(vm_image.image_path); EXPECT_THAT(image_data, StrEq(expected_data)); @@ -326,8 +399,13 @@ TEST_F(ImageVault, image_purged_expired) mpt::make_file_with_content(file_name); return {file_name, source_image.id, "", "", "", {}}; }; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_TRUE(QFileInfo::exists(file_name)); @@ -347,8 +425,13 @@ TEST_F(ImageVault, image_exists_not_expired) mpt::make_file_with_content(file_name); return {file_name, source_image.id, "", "", "", {}}; }; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_TRUE(QFileInfo::exists(file_name)); @@ -383,7 +466,13 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(file_based_fetch_copies_image_an query.release = file.url().toStdString(); query.query_type = mp::Query::Type::LocalFile; - auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_TRUE(QFileInfo::exists(vm_image.image_path)); EXPECT_EQ(vm_image.id, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); @@ -397,7 +486,13 @@ TEST_F(ImageVault, invalid_custom_image_file_throws) query.release = "file://foo"; query.query_type = mp::Query::Type::LocalFile; - EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), std::runtime_error); } @@ -409,7 +504,7 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(custom_image_url_downloads)) query.release = "http://www.foo.com/fake.img"; query.query_type = mp::Query::Type::HttpDownload; - vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt, instance_dir); EXPECT_THAT(url_downloader.downloaded_files.size(), Eq(1)); EXPECT_TRUE(url_downloader.downloaded_urls.contains(QString::fromStdString(query.release))); @@ -419,18 +514,28 @@ TEST_F(ImageVault, missing_downloaded_image_throws) { mpt::StubURLDownloader stub_url_downloader; mp::DefaultVMImageVault vault{hosts, &stub_url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - EXPECT_THROW( - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt), - mp::CreateImageException); + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), + mp::CreateImageException); } TEST_F(ImageVault, hash_mismatch_throws) { BadURLDownloader bad_url_downloader; mp::DefaultVMImageVault vault{hosts, &bad_url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - EXPECT_THROW( - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt), - mp::CreateImageException); + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), + mp::CreateImageException); } TEST_F(ImageVault, invalid_remote_throws) @@ -441,7 +546,13 @@ TEST_F(ImageVault, invalid_remote_throws) query.remote_name = "foo"; - EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), std::runtime_error); } @@ -453,7 +564,13 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(invalid_image_alias_throw)) query.release = "foo"; - EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), mp::CreateImageException); } @@ -466,8 +583,13 @@ TEST_F(ImageVault, valid_remote_and_alias_returns_valid_image_info) query.remote_name = "release"; mp::VMImage image; - EXPECT_NO_THROW( - image = vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt)); + EXPECT_NO_THROW(image = vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir)); EXPECT_THAT(image.original_release, Eq("18.04 LTS")); EXPECT_THAT(image.id, Eq("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); @@ -482,8 +604,13 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(http_download_returns_expected_i mp::Query query{instance_name, image_url, false, "", mp::Query::Type::HttpDownload}; mp::VMImage image; - EXPECT_NO_THROW( - image = vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt)); + EXPECT_NO_THROW(image = vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir)); // Hash is based on image url EXPECT_THAT(image.id, Eq("7404f51c9b4f40312fa048a0ad36e07b74b718a2d3a5a08e8cca906c69059ddf")); @@ -493,7 +620,13 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(http_download_returns_expected_i TEST_F(ImageVault, image_update_creates_new_dir_and_removes_old) { mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{1}}; - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); auto original_file{url_downloader.downloaded_files[0]}; auto original_absolute_path{QFileInfo(original_file).absolutePath()}; @@ -524,9 +657,14 @@ TEST_F(ImageVault, aborted_download_throws) running_url_downloader.abort_all_downloads(); - EXPECT_THROW( - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt), - mp::AbortedDownloadException); + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), + mp::AbortedDownloadException); } TEST_F(ImageVault, minimum_image_size_returns_expected_size) @@ -537,8 +675,13 @@ TEST_F(ImageVault, minimum_image_size_returns_expected_size) auto mock_factory_scope = inject_fake_qemuimg_callback(qemuimg_exit_status, qemuimg_output); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); const auto size = vault.minimum_image_size_for(vm_image.id); @@ -559,7 +702,13 @@ TEST_F(ImageVault, DISABLE_ON_WINDOWS_AND_MACOS(file_based_minimum_size_returns_ query.release = file.url().toStdString(); query.query_type = mp::Query::Type::LocalFile; - auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); const auto size = vault.minimum_image_size_for(vm_image.id); @@ -582,8 +731,13 @@ TEST_F(ImageVault, minimum_image_size_throws_when_qemuimg_info_crashes) auto mock_factory_scope = inject_fake_qemuimg_callback(qemuimg_exit_status, qemuimg_output); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); MP_EXPECT_THROW_THAT(vault.minimum_image_size_for(vm_image.id), std::runtime_error, mpt::match_what(AllOf(HasSubstr("qemu-img failed"), HasSubstr("with output")))); @@ -596,8 +750,13 @@ TEST_F(ImageVault, minimum_image_size_throws_when_qemuimg_info_cannot_find_the_i auto mock_factory_scope = inject_fake_qemuimg_callback(qemuimg_exit_status, qemuimg_output); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); MP_EXPECT_THROW_THAT(vault.minimum_image_size_for(vm_image.id), std::runtime_error, mpt::match_what(AllOf(HasSubstr("qemu-img failed"), HasSubstr("Could not find")))); @@ -610,8 +769,13 @@ TEST_F(ImageVault, minimum_image_size_throws_when_qemuimg_info_does_not_understa auto mock_factory_scope = inject_fake_qemuimg_callback(qemuimg_exit_status, qemuimg_output); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{0}}; - auto vm_image = - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + auto vm_image = vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); MP_EXPECT_THROW_THAT(vault.minimum_image_size_for(vm_image.id), std::runtime_error, mpt::match_what(HasSubstr("Could not obtain image's virtual size"))); @@ -682,7 +846,13 @@ TEST_F(ImageVault, updateImagesLogsWarningOnUnsupportedImage) { mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(mpl::Level::warning); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{1}}; - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_CALL(host, info_for(_)).WillOnce(Throw(mp::UnsupportedImageException(default_query.release))); @@ -699,7 +869,13 @@ TEST_F(ImageVault, updateImagesLogsWarningOnEmptyVault) { mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(mpl::Level::warning); mp::DefaultVMImageVault vault{hosts, &url_downloader, cache_dir.path(), data_dir.path(), mp::days{1}}; - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt); + vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir); EXPECT_CALL(host, info_for(_)).WillOnce(Return(std::nullopt)); @@ -719,9 +895,14 @@ TEST_F(ImageVault, fetchLocalImageThrowsOnEmptyVault) EXPECT_CALL(host, info_for(_)).WillOnce(Return(std::nullopt)); - EXPECT_THROW( - vault.fetch_image(mp::FetchType::ImageOnly, default_query, stub_prepare, stub_monitor, false, std::nullopt), - mp::ImageNotFoundException); + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + default_query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), + mp::ImageNotFoundException); } TEST_F(ImageVault, fetchRemoteImageThrowsOnMissingKernel) @@ -731,6 +912,12 @@ TEST_F(ImageVault, fetchRemoteImageThrowsOnMissingKernel) EXPECT_CALL(host, info_for(_)).WillOnce(Return(std::nullopt)); - EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, query, stub_prepare, stub_monitor, false, std::nullopt), + EXPECT_THROW(vault.fetch_image(mp::FetchType::ImageOnly, + query, + stub_prepare, + stub_monitor, + false, + std::nullopt, + instance_dir), mp::ImageNotFoundException); } diff --git a/tests/test_instance_settings_handler.cpp b/tests/test_instance_settings_handler.cpp index d5954bbe397..12c9eeeeafe 100644 --- a/tests/test_instance_settings_handler.cpp +++ b/tests/test_instance_settings_handler.cpp @@ -20,9 +20,9 @@ #include #include +#include #include -#include #include diff --git a/tests/test_output_formatter.cpp b/tests/test_output_formatter.cpp index 1d672cf24cc..b895d2b0717 100644 --- a/tests/test_output_formatter.cpp +++ b/tests/test_output_formatter.cpp @@ -40,11 +40,25 @@ auto petenv_name() return MP_SETTINGS.get(mp::petenv_key).toStdString(); } +auto construct_empty_list_reply() +{ + mp::ListReply list_reply; + list_reply.mutable_instance_list(); + return list_reply; +} + +auto construct_empty_list_snapshot_reply() +{ + mp::ListReply list_reply; + list_reply.mutable_snapshot_list(); + return list_reply; +} + auto construct_single_instance_list_reply() { mp::ListReply list_reply; - auto list_entry = list_reply.add_instances(); + auto list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("foo"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); list_entry->set_current_release("16.04 LTS"); @@ -60,13 +74,13 @@ auto construct_multiple_instances_list_reply() { mp::ListReply list_reply; - auto list_entry = list_reply.add_instances(); + auto list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("bogus-instance"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); list_entry->set_current_release("16.04 LTS"); list_entry->add_ipv4("10.21.124.56"); - list_entry = list_reply.add_instances(); + list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("bombastic"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::STOPPED); list_entry->set_current_release("18.04 LTS"); @@ -78,22 +92,22 @@ auto construct_unsorted_list_reply() { mp::ListReply list_reply; - auto list_entry = list_reply.add_instances(); + auto list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("trusty-190611-1542"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); list_entry->set_current_release("N/A"); - list_entry = list_reply.add_instances(); + list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("trusty-190611-1535"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::STOPPED); list_entry->set_current_release("N/A"); - list_entry = list_reply.add_instances(); + list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("trusty-190611-1539"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::SUSPENDED); list_entry->set_current_release(""); - list_entry = list_reply.add_instances(); + list_entry = list_reply.mutable_instance_list()->add_instances(); list_entry->set_name("trusty-190611-1529"); list_entry->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); list_entry->set_current_release(""); @@ -101,12 +115,92 @@ auto construct_unsorted_list_reply() return list_reply; } +auto construct_single_snapshot_list_reply() +{ + mp::ListReply list_reply; + + auto list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + list_entry->set_name("foo"); + + auto fundamentals = list_entry->mutable_fundamentals(); + fundamentals->set_snapshot_name("snapshot1"); + fundamentals->set_comment("This is a sample comment"); + + google::protobuf::Timestamp timestamp; + timestamp.set_seconds(time(nullptr)); + timestamp.set_nanos(0); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + return list_reply; +} + +auto construct_multiple_snapshots_list_reply() +{ + mp::ListReply list_reply; + + auto list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + auto fundamentals = list_entry->mutable_fundamentals(); + google::protobuf::Timestamp timestamp; + + list_entry->set_name("prosperous-spadefish"); + fundamentals->set_snapshot_name("snapshot10"); + fundamentals->set_parent("snapshot2"); + timestamp.set_seconds(1672531200); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + fundamentals = list_entry->mutable_fundamentals(); + list_entry->set_name("hale-roller"); + fundamentals->set_snapshot_name("rolling"); + fundamentals->set_parent("pristine"); + fundamentals->set_comment("Loaded with stuff"); + timestamp.set_seconds(25425952800); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + fundamentals = list_entry->mutable_fundamentals(); + list_entry->set_name("hale-roller"); + fundamentals->set_snapshot_name("rocking"); + fundamentals->set_parent("pristine"); + fundamentals->set_comment("A very long comment that should be truncated by the table formatter"); + timestamp.set_seconds(2209234259); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + fundamentals = list_entry->mutable_fundamentals(); + list_entry->set_name("hale-roller"); + fundamentals->set_snapshot_name("pristine"); + fundamentals->set_comment("A first snapshot"); + timestamp.set_seconds(409298914); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + list_entry = list_reply.mutable_snapshot_list()->add_snapshots(); + fundamentals = list_entry->mutable_fundamentals(); + list_entry->set_name("prosperous-spadefish"); + fundamentals->set_snapshot_name("snapshot2"); + fundamentals->set_comment("Before restoring snap1\nContains a newline that\r\nshould be truncated"); + timestamp.set_seconds(1671840000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + return list_reply; +} + auto add_petenv_to_reply(mp::ListReply& reply) { - auto instance = reply.add_instances(); - instance->set_name(petenv_name()); - instance->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); - instance->set_current_release("Not Available"); + if (reply.has_instance_list()) + { + auto instance = reply.mutable_instance_list()->add_instances(); + instance->set_name(petenv_name()); + instance->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); + instance->set_current_release("Not Available"); + } + else + { + auto snapshot = reply.mutable_snapshot_list()->add_snapshots(); + snapshot->set_name(petenv_name()); + snapshot->mutable_fundamentals()->set_snapshot_name("snapshot1"); + snapshot->mutable_fundamentals()->set_comment("An exemplary comment"); + } } auto construct_one_short_line_networks_reply() @@ -148,15 +242,22 @@ auto construct_multiple_lines_networks_reply() return networks_reply; } +auto construct_empty_info_snapshot_reply() +{ + mp::InfoReply info_reply; + info_reply.set_snapshots(true); + return info_reply; +} + auto construct_single_instance_info_reply() { mp::InfoReply info_reply; - auto info_entry = info_reply.add_info(); + auto info_entry = info_reply.add_details(); info_entry->set_name("foo"); info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); - info_entry->set_image_release("16.04 LTS"); - info_entry->set_id("1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac"); + info_entry->mutable_instance_info()->set_image_release("16.04 LTS"); + info_entry->mutable_instance_info()->set_id("1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac"); auto mount_info = info_entry->mutable_mount_info(); mount_info->set_longest_path_len(19); @@ -186,16 +287,17 @@ auto construct_single_instance_info_reply() gid_map_pair->set_instance_id(1000); info_entry->set_cpu_count("1"); - info_entry->set_load("0.45 0.51 0.15"); - info_entry->set_memory_usage("60817408"); + info_entry->mutable_instance_info()->set_load("0.45 0.51 0.15"); + info_entry->mutable_instance_info()->set_memory_usage("60817408"); info_entry->set_memory_total("1503238554"); - info_entry->set_disk_usage("1288490188"); + info_entry->mutable_instance_info()->set_disk_usage("1288490188"); info_entry->set_disk_total("5153960756"); - info_entry->set_current_release("Ubuntu 16.04.3 LTS"); - info_entry->add_ipv4("10.168.32.2"); - info_entry->add_ipv4("200.3.123.29"); - info_entry->add_ipv6("2001:67c:1562:8007::aac:423a"); - info_entry->add_ipv6("fd52:2ccf:f758:0:a342:79b5:e2ba:e05e"); + info_entry->mutable_instance_info()->set_current_release("Ubuntu 16.04.3 LTS"); + info_entry->mutable_instance_info()->add_ipv4("10.168.32.2"); + info_entry->mutable_instance_info()->add_ipv4("200.3.123.29"); + info_entry->mutable_instance_info()->add_ipv6("2001:67c:1562:8007::aac:423a"); + info_entry->mutable_instance_info()->add_ipv6("fd52:2ccf:f758:0:a342:79b5:e2ba:e05e"); + info_entry->mutable_instance_info()->set_num_snapshots(0); return info_reply; } @@ -204,11 +306,171 @@ auto construct_multiple_instances_info_reply() { mp::InfoReply info_reply; - auto info_entry = info_reply.add_info(); + auto info_entry = info_reply.add_details(); + info_entry->set_name("bogus-instance"); + info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); + info_entry->mutable_instance_info()->set_image_release("16.04 LTS"); + info_entry->mutable_instance_info()->set_id("1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac"); + + auto mount_info = info_entry->mutable_mount_info(); + mount_info->set_longest_path_len(17); + + auto mount_entry = mount_info->add_mount_paths(); + mount_entry->set_source_path("/home/user/source"); + mount_entry->set_target_path("source"); + + auto uid_map_pair = mount_entry->mutable_mount_maps()->add_uid_mappings(); + uid_map_pair->set_host_id(1000); + uid_map_pair->set_instance_id(501); + + auto gid_map_pair = mount_entry->mutable_mount_maps()->add_gid_mappings(); + gid_map_pair->set_host_id(1000); + gid_map_pair->set_instance_id(501); + + info_entry->set_cpu_count("4"); + info_entry->mutable_instance_info()->set_load("0.03 0.10 0.15"); + info_entry->mutable_instance_info()->set_memory_usage("38797312"); + info_entry->set_memory_total("1610612736"); + info_entry->mutable_instance_info()->set_disk_usage("1932735284"); + info_entry->set_disk_total("6764573492"); + info_entry->mutable_instance_info()->set_current_release("Ubuntu 16.04.3 LTS"); + info_entry->mutable_instance_info()->add_ipv4("10.21.124.56"); + info_entry->mutable_instance_info()->set_num_snapshots(1); + + info_entry = info_reply.add_details(); + info_entry->set_name("bombastic"); + info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::STOPPED); + info_entry->mutable_instance_info()->set_image_release("18.04 LTS"); + info_entry->mutable_instance_info()->set_id("ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509"); + info_entry->mutable_instance_info()->set_num_snapshots(3); + + return info_reply; +} + +auto construct_single_snapshot_info_reply() +{ + mp::InfoReply info_reply; + + auto info_entry = info_reply.add_details(); + auto fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("bogus-instance"); + info_entry->set_cpu_count("2"); + info_entry->set_disk_total("4.9GiB"); + info_entry->set_memory_total("0.9GiB"); + fundamentals->set_snapshot_name("snapshot2"); + fundamentals->set_parent("snapshot1"); + fundamentals->set_comment("This is a comment with some\nnew\r\nlines."); + info_entry->mutable_snapshot_info()->set_size("128MiB"); + info_entry->mutable_snapshot_info()->add_children("snapshot3"); + info_entry->mutable_snapshot_info()->add_children("snapshot4"); + + auto mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user/source"); + mount_entry->set_target_path("source"); + mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user"); + mount_entry->set_target_path("Home"); + + google::protobuf::Timestamp timestamp; + timestamp.set_seconds(63108020); + timestamp.set_nanos(21000000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + return info_reply; +} + +auto construct_multiple_snapshots_info_reply() +{ + mp::InfoReply info_reply; + + auto info_entry = info_reply.add_details(); + auto fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("messier-87"); + info_entry->set_cpu_count("1"); + info_entry->set_disk_total("1024GiB"); + info_entry->set_memory_total("128GiB"); + fundamentals->set_snapshot_name("black-hole"); + fundamentals->set_comment("Captured by EHT"); + + google::protobuf::Timestamp timestamp; + timestamp.set_seconds(1554897599); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + info_entry = info_reply.add_details(); + fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("bogus-instance"); + info_entry->set_cpu_count("2"); + info_entry->set_disk_total("4.9GiB"); + info_entry->set_memory_total("0.9GiB"); + fundamentals->set_snapshot_name("snapshot2"); + fundamentals->set_parent("snapshot1"); + info_entry->mutable_snapshot_info()->add_children("snapshot3"); + info_entry->mutable_snapshot_info()->add_children("snapshot4"); + + auto mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user/source"); + mount_entry->set_target_path("source"); + mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user"); + mount_entry->set_target_path("Home"); + + timestamp.set_seconds(63108020); + timestamp.set_nanos(21000000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + return info_reply; +} + +auto construct_mixed_instance_and_snapshot_info_reply() +{ + mp::InfoReply info_reply; + + auto info_entry = info_reply.add_details(); + auto fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("bogus-instance"); + info_entry->set_cpu_count("2"); + info_entry->set_disk_total("4.9GiB"); + info_entry->set_memory_total("0.9GiB"); + fundamentals->set_snapshot_name("snapshot2"); + fundamentals->set_parent("snapshot1"); + info_entry->mutable_snapshot_info()->add_children("snapshot3"); + info_entry->mutable_snapshot_info()->add_children("snapshot4"); + + auto mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user/source"); + mount_entry->set_target_path("source"); + mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user"); + mount_entry->set_target_path("Home"); + + google::protobuf::Timestamp timestamp; + timestamp.set_seconds(63108020); + timestamp.set_nanos(21000000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + info_entry = info_reply.add_details(); + info_entry->set_name("bombastic"); + info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::STOPPED); + info_entry->mutable_instance_info()->set_image_release("18.04 LTS"); + info_entry->mutable_instance_info()->set_id("ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509"); + info_entry->mutable_instance_info()->set_num_snapshots(3); + + return info_reply; +} + +auto construct_multiple_mixed_instances_and_snapshots_info_reply() +{ + mp::InfoReply info_reply; + + auto info_entry = info_reply.add_details(); info_entry->set_name("bogus-instance"); info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::RUNNING); - info_entry->set_image_release("16.04 LTS"); - info_entry->set_id("1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac"); + info_entry->mutable_instance_info()->set_image_release("16.04 LTS"); + info_entry->mutable_instance_info()->set_id("1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac"); auto mount_info = info_entry->mutable_mount_info(); mount_info->set_longest_path_len(17); @@ -226,30 +488,93 @@ auto construct_multiple_instances_info_reply() gid_map_pair->set_instance_id(501); info_entry->set_cpu_count("4"); - info_entry->set_load("0.03 0.10 0.15"); - info_entry->set_memory_usage("38797312"); + info_entry->mutable_instance_info()->set_load("0.03 0.10 0.15"); + info_entry->mutable_instance_info()->set_memory_usage("38797312"); info_entry->set_memory_total("1610612736"); - info_entry->set_disk_usage("1932735284"); + info_entry->mutable_instance_info()->set_disk_usage("1932735284"); info_entry->set_disk_total("6764573492"); - info_entry->set_current_release("Ubuntu 16.04.3 LTS"); - info_entry->add_ipv4("10.21.124.56"); + info_entry->mutable_instance_info()->set_current_release("Ubuntu 16.04.3 LTS"); + info_entry->mutable_instance_info()->add_ipv4("10.21.124.56"); + info_entry->mutable_instance_info()->set_num_snapshots(2); + + info_entry = info_reply.add_details(); + auto fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("bogus-instance"); + info_entry->set_cpu_count("2"); + info_entry->set_disk_total("4.9GiB"); + info_entry->set_memory_total("0.9GiB"); + fundamentals->set_snapshot_name("snapshot2"); + fundamentals->set_parent("snapshot1"); + info_entry->mutable_snapshot_info()->add_children("snapshot3"); + info_entry->mutable_snapshot_info()->add_children("snapshot4"); + + mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user/source"); + mount_entry->set_target_path("source"); + mount_entry = info_entry->mutable_mount_info()->add_mount_paths(); + mount_entry->set_source_path("/home/user"); + mount_entry->set_target_path("Home"); + + google::protobuf::Timestamp timestamp; + timestamp.set_seconds(63108020); + timestamp.set_nanos(21000000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + info_entry = info_reply.add_details(); + fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("bogus-instance"); + info_entry->set_cpu_count("2"); + info_entry->set_disk_total("4.9GiB"); + info_entry->set_memory_total("0.9GiB"); + fundamentals->set_snapshot_name("snapshot1"); - info_entry = info_reply.add_info(); + timestamp.set_seconds(63107999); + timestamp.set_nanos(21000000); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); + + info_entry = info_reply.add_details(); info_entry->set_name("bombastic"); info_entry->mutable_instance_status()->set_status(mp::InstanceStatus::STOPPED); - info_entry->set_image_release("18.04 LTS"); - info_entry->set_id("ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509"); + info_entry->mutable_instance_info()->set_image_release("18.04 LTS"); + info_entry->mutable_instance_info()->set_id("ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509"); + info_entry->mutable_instance_info()->set_num_snapshots(3); + + info_entry = info_reply.add_details(); + fundamentals = info_entry->mutable_snapshot_info()->mutable_fundamentals(); + + info_entry->set_name("messier-87"); + info_entry->set_cpu_count("1"); + info_entry->set_disk_total("1024GiB"); + info_entry->set_memory_total("128GiB"); + fundamentals->set_snapshot_name("black-hole"); + fundamentals->set_comment("Captured by EHT"); + + timestamp.set_seconds(1554897599); + timestamp.set_nanos(0); + fundamentals->mutable_creation_timestamp()->CopyFrom(timestamp); return info_reply; } -auto add_petenv_to_reply(mp::InfoReply& reply) +auto add_petenv_to_reply(mp::InfoReply& reply, bool csv_format, bool snapshots) { - auto entry = reply.add_info(); - entry->set_name(petenv_name()); - entry->mutable_instance_status()->set_status(mp::InstanceStatus::SUSPENDED); - entry->set_image_release("18.10"); - entry->set_id("1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"); + if ((csv_format && !snapshots) || !csv_format) + { + auto entry = reply.add_details(); + entry->set_name(petenv_name()); + entry->mutable_instance_status()->set_status(mp::InstanceStatus::SUSPENDED); + entry->mutable_instance_info()->set_image_release("18.10"); + entry->mutable_instance_info()->set_id("1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"); + } + + if ((csv_format && snapshots) || !csv_format) + { + auto entry = reply.add_details(); + entry->set_name(petenv_name()); + entry->mutable_snapshot_info()->mutable_fundamentals()->set_snapshot_name("snapshot1"); + } } auto construct_empty_reply() @@ -470,10 +795,13 @@ const mp::JsonFormatter json_formatter; const mp::CSVFormatter csv_formatter; const mp::YamlFormatter yaml_formatter; -const auto empty_list_reply = mp::ListReply(); +const auto empty_list_reply = construct_empty_list_reply(); +const auto empty_list_snapshot_reply = construct_empty_list_snapshot_reply(); const auto single_instance_list_reply = construct_single_instance_list_reply(); const auto multiple_instances_list_reply = construct_multiple_instances_list_reply(); const auto unsorted_list_reply = construct_unsorted_list_reply(); +const auto single_snapshot_list_reply = construct_single_snapshot_list_reply(); +const auto multiple_snapshots_list_reply = construct_multiple_snapshots_list_reply(); const auto empty_networks_reply = mp::NetworksReply(); const auto one_short_line_networks_reply = construct_one_short_line_networks_reply(); @@ -481,35 +809,62 @@ const auto one_long_line_networks_reply = construct_one_long_line_networks_reply const auto multiple_lines_networks_reply = construct_multiple_lines_networks_reply(); const auto empty_info_reply = mp::InfoReply(); +const auto empty_info_snapshot_reply = construct_empty_info_snapshot_reply(); const auto single_instance_info_reply = construct_single_instance_info_reply(); const auto multiple_instances_info_reply = construct_multiple_instances_info_reply(); +const auto single_snapshot_info_reply = construct_single_snapshot_info_reply(); +const auto multiple_snapshots_info_reply = construct_multiple_snapshots_info_reply(); +const auto mixed_instance_and_snapshot_info_reply = construct_mixed_instance_and_snapshot_info_reply(); +const auto multiple_mixed_instances_and_snapshots_info_reply = + construct_multiple_mixed_instances_and_snapshots_info_reply(); const std::vector orderable_list_info_formatter_outputs{ {&table_formatter, &empty_list_reply, "No instances found.\n", "table_list_empty"}, - {&table_formatter, &single_instance_list_reply, + {&table_formatter, &empty_list_snapshot_reply, "No snapshots found.\n", "table_list_snapshot_empty"}, + {&table_formatter, + &single_instance_list_reply, "Name State IPv4 Image\n" "foo Running 10.168.32.2 Ubuntu 16.04 LTS\n" " 200.3.123.30\n", "table_list_single"}, - {&table_formatter, &multiple_instances_list_reply, + {&table_formatter, + &multiple_instances_list_reply, "Name State IPv4 Image\n" "bogus-instance Running 10.21.124.56 Ubuntu 16.04 LTS\n" "bombastic Stopped -- Ubuntu 18.04 LTS\n", "table_list_multiple"}, - {&table_formatter, &unsorted_list_reply, + {&table_formatter, + &unsorted_list_reply, "Name State IPv4 Image\n" "trusty-190611-1529 Deleted -- Not Available\n" "trusty-190611-1535 Stopped -- Ubuntu N/A\n" "trusty-190611-1539 Suspended -- Not Available\n" "trusty-190611-1542 Running -- Ubuntu N/A\n", "table_list_unsorted"}, - - {&table_formatter, &empty_info_reply, "\n", "table_info_empty"}, - {&table_formatter, &single_instance_info_reply, + {&table_formatter, + &single_snapshot_list_reply, + "Instance Snapshot Parent Comment\n" + "foo snapshot1 -- This is a sample comment\n", + "table_list_single_snapshot"}, + {&table_formatter, + &multiple_snapshots_list_reply, + "Instance Snapshot Parent Comment\n" + "hale-roller pristine -- A first snapshot\n" + "hale-roller rocking pristine A very long comment that should be truncated by t…\n" + "hale-roller rolling pristine Loaded with stuff\n" + "prosperous-spadefish snapshot2 -- Before restoring snap1…\n" + "prosperous-spadefish snapshot10 snapshot2 --\n", + "table_list_multiple_snapshots"}, + + {&table_formatter, &empty_info_reply, "No instances found.\n", "table_info_empty"}, + {&table_formatter, &empty_info_snapshot_reply, "No snapshots found.\n", "table_info_snapshot_empty"}, + {&table_formatter, + &single_instance_info_reply, "Name: foo\n" "State: Running\n" + "Snapshots: 0\n" "IPv4: 10.168.32.2\n" " 200.3.123.29\n" "IPv6: 2001:67c:1562:8007::aac:423a\n" @@ -526,10 +881,12 @@ const std::vector orderable_list_info_formatter_outputs{ " /home/user/test_dir => test_dir\n" " UID map: 1000:1000\n" " GID map: 1000:1000\n", - "table_info_single"}, - {&table_formatter, &multiple_instances_info_reply, + "table_info_single_instance"}, + {&table_formatter, + &multiple_instances_info_reply, "Name: bogus-instance\n" "State: Running\n" + "Snapshots: 1\n" "IPv4: 10.21.124.56\n" "Release: Ubuntu 16.04.3 LTS\n" "Image hash: 1797c5c82016 (Ubuntu 16.04 LTS)\n" @@ -542,6 +899,7 @@ const std::vector orderable_list_info_formatter_outputs{ " GID map: 1000:501\n\n" "Name: bombastic\n" "State: Stopped\n" + "Snapshots: 3\n" "IPv4: --\n" "Release: --\n" "Image hash: ab5191cc1725 (Ubuntu 18.04 LTS)\n" @@ -550,48 +908,209 @@ const std::vector orderable_list_info_formatter_outputs{ "Disk usage: --\n" "Memory usage: --\n" "Mounts: --\n", - "table_info_multiple"}, + "table_info_multiple_instances"}, + {&table_formatter, + &single_snapshot_info_reply, + "Snapshot: snapshot2\n" + "Instance: bogus-instance\n" + "Size: 128MiB\n" + "CPU(s): 2\n" + "Disk space: 4.9GiB\n" + "Memory size: 0.9GiB\n" + "Mounts: /home/user/source => source\n" + " /home/user => Home\n" + "Created: 1972-01-01T10:00:20.021Z\n" + "Parent: snapshot1\n" + "Children: snapshot3\n" + " snapshot4\n" + "Comment: This is a comment with some\n" + " new\r\n" + " lines.\n", + "table_info_single_snapshot"}, + {&table_formatter, + &multiple_snapshots_info_reply, + "Snapshot: snapshot2\n" + "Instance: bogus-instance\n" + "CPU(s): 2\n" + "Disk space: 4.9GiB\n" + "Memory size: 0.9GiB\n" + "Mounts: /home/user/source => source\n" + " /home/user => Home\n" + "Created: 1972-01-01T10:00:20.021Z\n" + "Parent: snapshot1\n" + "Children: snapshot3\n" + " snapshot4\n" + "Comment: --\n\n" + "Snapshot: black-hole\n" + "Instance: messier-87\n" + "CPU(s): 1\n" + "Disk space: 1024GiB\n" + "Memory size: 128GiB\n" + "Mounts: --\n" + "Created: 2019-04-10T11:59:59Z\n" + "Parent: --\n" + "Children: --\n" + "Comment: Captured by EHT\n", + "table_info_multiple_snapshots"}, + {&table_formatter, + &mixed_instance_and_snapshot_info_reply, + "Name: bombastic\n" + "State: Stopped\n" + "Snapshots: 3\n" + "IPv4: --\n" + "Release: --\n" + "Image hash: ab5191cc1725 (Ubuntu 18.04 LTS)\n" + "CPU(s): --\n" + "Load: --\n" + "Disk usage: --\n" + "Memory usage: --\n" + "Mounts: --\n\n" + "Snapshot: snapshot2\n" + "Instance: bogus-instance\n" + "CPU(s): 2\n" + "Disk space: 4.9GiB\n" + "Memory size: 0.9GiB\n" + "Mounts: /home/user/source => source\n" + " /home/user => Home\n" + "Created: 1972-01-01T10:00:20.021Z\n" + "Parent: snapshot1\n" + "Children: snapshot3\n" + " snapshot4\n" + "Comment: --\n", + "table_info_mixed_instance_and_snapshot"}, + {&table_formatter, + &multiple_mixed_instances_and_snapshots_info_reply, + "Name: bogus-instance\n" + "State: Running\n" + "Snapshots: 2\n" + "IPv4: 10.21.124.56\n" + "Release: Ubuntu 16.04.3 LTS\n" + "Image hash: 1797c5c82016 (Ubuntu 16.04 LTS)\n" + "CPU(s): 4\n" + "Load: 0.03 0.10 0.15\n" + "Disk usage: 1.8GiB out of 6.3GiB\n" + "Memory usage: 37.0MiB out of 1.5GiB\n" + "Mounts: /home/user/source => source\n" + " UID map: 1000:501\n" + " GID map: 1000:501\n\n" + "Name: bombastic\n" + "State: Stopped\n" + "Snapshots: 3\n" + "IPv4: --\n" + "Release: --\n" + "Image hash: ab5191cc1725 (Ubuntu 18.04 LTS)\n" + "CPU(s): --\n" + "Load: --\n" + "Disk usage: --\n" + "Memory usage: --\n" + "Mounts: --\n\n" + "Snapshot: snapshot1\n" + "Instance: bogus-instance\n" + "CPU(s): 2\n" + "Disk space: 4.9GiB\n" + "Memory size: 0.9GiB\n" + "Mounts: --\n" + "Created: 1972-01-01T09:59:59.021Z\n" + "Parent: --\n" + "Children: --\n" + "Comment: --\n\n" + "Snapshot: snapshot2\n" + "Instance: bogus-instance\n" + "CPU(s): 2\n" + "Disk space: 4.9GiB\n" + "Memory size: 0.9GiB\n" + "Mounts: /home/user/source => source\n" + " /home/user => Home\n" + "Created: 1972-01-01T10:00:20.021Z\n" + "Parent: snapshot1\n" + "Children: snapshot3\n" + " snapshot4\n" + "Comment: --\n\n" + "Snapshot: black-hole\n" + "Instance: messier-87\n" + "CPU(s): 1\n" + "Disk space: 1024GiB\n" + "Memory size: 128GiB\n" + "Mounts: --\n" + "Created: 2019-04-10T11:59:59Z\n" + "Parent: --\n" + "Children: --\n" + "Comment: Captured by EHT\n", + "table_info_multiple_mixed_instances_and_snapshots"}, {&csv_formatter, &empty_list_reply, "Name,State,IPv4,IPv6,Release,AllIPv4\n", "csv_list_empty"}, - {&csv_formatter, &single_instance_list_reply, + {&csv_formatter, + &single_instance_list_reply, "Name,State,IPv4,IPv6,Release,AllIPv4\n" "foo,Running,10.168.32.2,fdde:2681:7a2::4ca,Ubuntu 16.04 LTS,\"10.168.32.2,200.3.123.30\"\n", "csv_list_single"}, - {&csv_formatter, &multiple_instances_list_reply, + {&csv_formatter, + &multiple_instances_list_reply, "Name,State,IPv4,IPv6,Release,AllIPv4\n" "bogus-instance,Running,10.21.124.56,,Ubuntu 16.04 LTS,\"10.21.124.56\"\n" "bombastic,Stopped,,,Ubuntu 18.04 LTS,\"\"\n", "csv_list_multiple"}, - {&csv_formatter, &unsorted_list_reply, + {&csv_formatter, + &unsorted_list_reply, "Name,State,IPv4,IPv6,Release,AllIPv4\n" "trusty-190611-1529,Deleted,,,Not Available,\"\"\n" "trusty-190611-1535,Stopped,,,Ubuntu N/A,\"\"\n" "trusty-190611-1539,Suspended,,,Not Available,\"\"\n" "trusty-190611-1542,Running,,,Ubuntu N/A,\"\"\n", "csv_list_unsorted"}, - - {&csv_formatter, &empty_info_reply, + {&csv_formatter, &empty_list_snapshot_reply, "Instance,Snapshot,Parent,Comment\n", "csv_list_snapshot_empty"}, + {&csv_formatter, + &single_snapshot_list_reply, + "Instance,Snapshot,Parent,Comment\nfoo,snapshot1,,\"This is a sample comment\"\n", + "csv_list_single_snapshot"}, + {&csv_formatter, + &multiple_snapshots_list_reply, + "Instance,Snapshot,Parent,Comment\nhale-roller,pristine,,\"A first " + "snapshot\"\nhale-roller,rocking,pristine,\"A very long comment that should be truncated by the table " + "formatter\"\nhale-roller,rolling,pristine,\"Loaded with stuff\"\nprosperous-spadefish,snapshot2,,\"Before " + "restoring snap1\nContains a newline that\r\nshould be " + "truncated\"\nprosperous-spadefish,snapshot10,snapshot2,\"\"\n", + "csv_list_multiple_snapshots"}, + + {&csv_formatter, &empty_info_reply, "", "csv_info_empty"}, + {&csv_formatter, + &single_instance_info_reply, "Name,State,Ipv4,Ipv6,Release,Image hash,Image release,Load,Disk usage,Disk total,Memory " - "usage,Memory total,Mounts,AllIPv4,CPU(s)\n", - "csv_info_empty"}, - {&csv_formatter, &single_instance_info_reply, - "Name,State,Ipv4,Ipv6,Release,Image hash,Image release,Load,Disk usage,Disk total,Memory " - "usage,Memory total,Mounts,AllIPv4,CPU(s)\nfoo,Running,10.168.32.2,2001:67c:1562:8007::aac:423a,Ubuntu 16.04.3 " + "usage,Memory total,Mounts,AllIPv4,CPU(s),Snapshots\nfoo,Running,10.168.32.2,2001:67c:1562:8007::aac:423a,Ubuntu " + "16.04.3 " "LTS,1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac,16.04 LTS,0.45 0.51 " "0.15,1288490188,5153960756,60817408,1503238554,/home/user/foo => foo;/home/user/test_dir " - "=> test_dir;,\"10.168.32.2,200.3.123.29\";,1\n", - "csv_info_single"}, - {&csv_formatter, &multiple_instances_info_reply, + "=> test_dir,10.168.32.2;200.3.123.29,1,0\n", + "csv_info_single_instance"}, + {&csv_formatter, + &single_snapshot_info_reply, + "Snapshot,Instance,CPU(s),Disk space,Memory " + "size,Mounts,Created,Parent,Children,Comment\nsnapshot2,bogus-instance,2,4.9GiB,0.9GiB,/home/user/source " + "=> " + "source;/home/user => Home,1972-01-01T10:00:20.021Z,snapshot1,snapshot3;snapshot4,\"This is a comment with " + "some\nnew\r\nlines.\"\n", + "csv_info_single_snapshot_info_reply"}, + {&csv_formatter, + &multiple_snapshots_info_reply, + "Snapshot,Instance,CPU(s),Disk space,Memory " + "size,Mounts,Created,Parent,Children,Comment\nsnapshot2,bogus-instance,2,4.9GiB,0.9GiB,/home/user/source => " + "source;/home/user => " + "Home,1972-01-01T10:00:20.021Z,snapshot1,snapshot3;snapshot4,\"\"\nblack-hole,messier-87,1,1024GiB,128GiB,," + "2019-04-10T11:59:59Z,,,\"Captured by EHT\"\n", + "csv_info_multiple_snapshot_info_reply"}, + {&csv_formatter, + &multiple_instances_info_reply, "Name,State,Ipv4,Ipv6,Release,Image hash,Image release,Load,Disk usage,Disk total,Memory " - "usage,Memory total,Mounts,AllIPv4,CPU(s)\nbogus-instance,Running,10.21.124.56,,Ubuntu 16.04.3 " + "usage,Memory total,Mounts,AllIPv4,CPU(s),Snapshots\nbogus-instance,Running,10.21.124.56,,Ubuntu 16.04.3 " "LTS,1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac,16.04 LTS,0.03 0.10 " "0.15,1932735284,6764573492,38797312,1610612736,/home/user/source => " - "source;,\"10.21.124.56\";,4\nbombastic,Stopped,,,," - "ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509,18.04 LTS,,,,,,,\"\";,\n", - "csv_info_multiple"}, + "source,10.21.124.56,4,1\nbombastic,Stopped,,,," + "ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509,18.04 LTS,,,,,,,,,3\n", + "csv_info_multiple_instances"}, {&yaml_formatter, &empty_list_reply, "\n", "yaml_list_empty"}, - {&yaml_formatter, &single_instance_list_reply, + {&yaml_formatter, + &single_instance_list_reply, "foo:\n" " - state: Running\n" " ipv4:\n" @@ -599,7 +1118,8 @@ const std::vector orderable_list_info_formatter_outputs{ " - 200.3.123.30\n" " release: Ubuntu 16.04 LTS\n", "yaml_list_single"}, - {&yaml_formatter, &multiple_instances_list_reply, + {&yaml_formatter, + &multiple_instances_list_reply, "bogus-instance:\n" " - state: Running\n" " ipv4:\n" @@ -611,7 +1131,8 @@ const std::vector orderable_list_info_formatter_outputs{ " []\n" " release: Ubuntu 18.04 LTS\n", "yaml_list_multiple"}, - {&yaml_formatter, &unsorted_list_reply, + {&yaml_formatter, + &unsorted_list_reply, "trusty-190611-1529:\n" " - state: Deleted\n" " ipv4:\n" @@ -633,13 +1154,43 @@ const std::vector orderable_list_info_formatter_outputs{ " []\n" " release: Ubuntu N/A\n", "yaml_list_unsorted"}, - {&yaml_formatter, &empty_info_reply, "errors:\n - ~\n", "yaml_info_empty"}, + {&yaml_formatter, &empty_list_snapshot_reply, "\n", "yaml_list_snapshot_empty"}, + {&yaml_formatter, + &single_snapshot_list_reply, + "foo:\n" + " - snapshot1:\n" + " - parent: ~\n" + " comment: This is a sample comment\n", + "yaml_list_single_snapshot"}, + {&yaml_formatter, + &multiple_snapshots_list_reply, + "hale-roller:\n" + " - pristine:\n" + " - parent: ~\n" + " comment: A first snapshot\n" + " - rocking:\n" + " - parent: pristine\n" + " comment: A very long comment that should be truncated by the table formatter\n" + " - rolling:\n" + " - parent: pristine\n" + " comment: Loaded with stuff\n" + "prosperous-spadefish:\n" + " - snapshot2:\n" + " - parent: ~\n" + " comment: \"Before restoring snap1\\nContains a newline that\\r\\nshould be truncated\"\n" + " - snapshot10:\n" + " - parent: snapshot2\n" + " comment: ~\n", + "yaml_list_multiple_snapshots"}, - {&yaml_formatter, &single_instance_info_reply, + {&yaml_formatter, &empty_info_reply, "errors:\n - ~\n", "yaml_info_empty"}, + {&yaml_formatter, + &single_instance_info_reply, "errors:\n" " - ~\n" "foo:\n" " - state: Running\n" + " snapshot_count: 0\n" " image_hash: 1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac\n" " image_release: 16.04 LTS\n" " release: Ubuntu 16.04.3 LTS\n" @@ -671,12 +1222,14 @@ const std::vector orderable_list_info_formatter_outputs{ " gid_mappings:\n" " - \"1000:1000\"\n" " source_path: /home/user/test_dir\n", - "yaml_info_single"}, - {&yaml_formatter, &multiple_instances_info_reply, + "yaml_info_single_instance"}, + {&yaml_formatter, + &multiple_instances_info_reply, "errors:\n" " - ~\n" "bogus-instance:\n" " - state: Running\n" + " snapshot_count: 1\n" " image_hash: 1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac\n" " image_release: 16.04 LTS\n" " release: Ubuntu 16.04.3 LTS\n" @@ -703,6 +1256,7 @@ const std::vector orderable_list_info_formatter_outputs{ " source_path: /home/user/source\n" "bombastic:\n" " - state: Stopped\n" + " snapshot_count: 3\n" " image_hash: ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509\n" " image_release: 18.04 LTS\n" " release: ~\n" @@ -717,16 +1271,207 @@ const std::vector orderable_list_info_formatter_outputs{ " ipv4:\n" " []\n" " mounts: ~\n", - "yaml_info_multiple"}}; + "yaml_info_multiple_instances"}, + {&yaml_formatter, + &single_snapshot_info_reply, + "errors:\n" + " - ~\n" + "bogus-instance:\n" + " - snapshots:\n" + " - snapshot2:\n" + " size: 128MiB\n" + " cpu_count: 2\n" + " disk_space: 4.9GiB\n" + " memory_size: 0.9GiB\n" + " mounts:\n" + " source:\n" + " source_path: /home/user/source\n" + " Home:\n" + " source_path: /home/user\n" + " created: \"1972-01-01T10:00:20.021Z\"\n" + " parent: snapshot1\n" + " children:\n" + " - snapshot3\n" + " - snapshot4\n" + " comment: \"This is a comment with some\\nnew\\r\\nlines.\"\n", + "yaml_info_single_snapshot_info_reply"}, + {&yaml_formatter, + &multiple_snapshots_info_reply, + "errors:\n" + " - ~\n" + "bogus-instance:\n" + " - snapshots:\n" + " - snapshot2:\n" + " size: ~\n" + " cpu_count: 2\n" + " disk_space: 4.9GiB\n" + " memory_size: 0.9GiB\n" + " mounts:\n" + " source:\n" + " source_path: /home/user/source\n" + " Home:\n" + " source_path: /home/user\n" + " created: \"1972-01-01T10:00:20.021Z\"\n" + " parent: snapshot1\n" + " children:\n" + " - snapshot3\n" + " - snapshot4\n" + " comment: ~\n" + "messier-87:\n" + " - snapshots:\n" + " - black-hole:\n" + " size: ~\n" + " cpu_count: 1\n" + " disk_space: 1024GiB\n" + " memory_size: 128GiB\n" + " mounts: ~\n" + " created: \"2019-04-10T11:59:59Z\"\n" + " parent: ~\n" + " children:\n" + " []\n" + " comment: Captured by EHT\n", + "yaml_info_multiple_snapshots_info_reply"}, + {&yaml_formatter, + &mixed_instance_and_snapshot_info_reply, + "errors:\n" + " - ~\n" + "bombastic:\n" + " - state: Stopped\n" + " snapshot_count: 3\n" + " image_hash: ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509\n" + " image_release: 18.04 LTS\n" + " release: ~\n" + " cpu_count: ~\n" + " disks:\n" + " - sda1:\n" + " used: ~\n" + " total: ~\n" + " memory:\n" + " usage: ~\n" + " total: ~\n" + " ipv4:\n" + " []\n" + " mounts: ~\n" + "bogus-instance:\n" + " - snapshots:\n" + " - snapshot2:\n" + " size: ~\n" + " cpu_count: 2\n" + " disk_space: 4.9GiB\n" + " memory_size: 0.9GiB\n" + " mounts:\n" + " source:\n" + " source_path: /home/user/source\n" + " Home:\n" + " source_path: /home/user\n" + " created: \"1972-01-01T10:00:20.021Z\"\n" + " parent: snapshot1\n" + " children:\n" + " - snapshot3\n" + " - snapshot4\n" + " comment: ~\n", + "yaml_info_mixed_instance_and_snapshot_info_reply"}, + {&yaml_formatter, + &multiple_mixed_instances_and_snapshots_info_reply, + "errors:\n" + " - ~\n" + "bogus-instance:\n" + " - state: Running\n" + " snapshot_count: 2\n" + " image_hash: 1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac\n" + " image_release: 16.04 LTS\n" + " release: Ubuntu 16.04.3 LTS\n" + " cpu_count: 4\n" + " load:\n" + " - 0.03\n" + " - 0.10\n" + " - 0.15\n" + " disks:\n" + " - sda1:\n" + " used: 1932735284\n" + " total: 6764573492\n" + " memory:\n" + " usage: 38797312\n" + " total: 1610612736\n" + " ipv4:\n" + " - 10.21.124.56\n" + " mounts:\n" + " source:\n" + " uid_mappings:\n" + " - \"1000:501\"\n" + " gid_mappings:\n" + " - \"1000:501\"\n" + " source_path: /home/user/source\n" + " snapshots:\n" + " - snapshot1:\n" + " size: ~\n" + " cpu_count: 2\n" + " disk_space: 4.9GiB\n" + " memory_size: 0.9GiB\n" + " mounts: ~\n" + " created: \"1972-01-01T09:59:59.021Z\"\n" + " parent: ~\n" + " children:\n" + " []\n" + " comment: ~\n" + " - snapshot2:\n" + " size: ~\n" + " cpu_count: 2\n" + " disk_space: 4.9GiB\n" + " memory_size: 0.9GiB\n" + " mounts:\n" + " source:\n" + " source_path: /home/user/source\n" + " Home:\n" + " source_path: /home/user\n" + " created: \"1972-01-01T10:00:20.021Z\"\n" + " parent: snapshot1\n" + " children:\n" + " - snapshot3\n" + " - snapshot4\n" + " comment: ~\n" + "bombastic:\n" + " - state: Stopped\n" + " snapshot_count: 3\n" + " image_hash: ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509\n" + " image_release: 18.04 LTS\n" + " release: ~\n" + " cpu_count: ~\n" + " disks:\n" + " - sda1:\n" + " used: ~\n" + " total: ~\n" + " memory:\n" + " usage: ~\n" + " total: ~\n" + " ipv4:\n" + " []\n" + " mounts: ~\n" + "messier-87:\n" + " - snapshots:\n" + " - black-hole:\n" + " size: ~\n" + " cpu_count: 1\n" + " disk_space: 1024GiB\n" + " memory_size: 128GiB\n" + " mounts: ~\n" + " created: \"2019-04-10T11:59:59Z\"\n" + " parent: ~\n" + " children:\n" + " []\n" + " comment: Captured by EHT\n", + "yaml_info_multiple_mixed_instances_and_snapshots"}}; const std::vector non_orderable_list_info_formatter_outputs{ - {&json_formatter, &empty_list_reply, + {&json_formatter, + &empty_list_reply, "{\n" " \"list\": [\n" " ]\n" "}\n", "json_list_empty"}, - {&json_formatter, &single_instance_list_reply, + {&json_formatter, + &single_instance_list_reply, "{\n" " \"list\": [\n" " {\n" @@ -741,7 +1486,8 @@ const std::vector non_orderable_list_info_formatter_outputs{ " ]\n" "}\n", "json_list_single"}, - {&json_formatter, &multiple_instances_list_reply, + {&json_formatter, + &multiple_instances_list_reply, "{\n" " \"list\": [\n" " {\n" @@ -762,7 +1508,56 @@ const std::vector non_orderable_list_info_formatter_outputs{ " ]\n" "}\n", "json_list_multiple"}, - {&json_formatter, &empty_info_reply, + {&json_formatter, + &single_snapshot_list_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"foo\": {\n" + " \"snapshot1\": {\n" + " \"comment\": \"This is a sample comment\",\n" + " \"parent\": \"\"\n" + " }\n" + " }\n" + " }\n" + "}\n", + "json_list_single_snapshot"}, + {&json_formatter, + &multiple_snapshots_list_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"hale-roller\": {\n" + " \"pristine\": {\n" + " \"comment\": \"A first snapshot\",\n" + " \"parent\": \"\"\n" + " },\n" + " \"rocking\": {\n" + " \"comment\": \"A very long comment that should be truncated by the table formatter\",\n" + " \"parent\": \"pristine\"\n" + " },\n" + " \"rolling\": {\n" + " \"comment\": \"Loaded with stuff\",\n" + " \"parent\": \"pristine\"\n" + " }\n" + " },\n" + " \"prosperous-spadefish\": {\n" + " \"snapshot10\": {\n" + " \"comment\": \"\",\n" + " \"parent\": \"snapshot2\"\n" + " },\n" + " \"snapshot2\": {\n" + " \"comment\": \"Before restoring snap1\\nContains a newline that\\r\\nshould be truncated\",\n" + " \"parent\": \"\"\n" + " }\n" + " }\n" + " }\n" + "}\n", + "json_list_multiple_snapshots"}, + {&json_formatter, + &empty_info_reply, "{\n" " \"errors\": [\n" " ],\n" @@ -770,7 +1565,8 @@ const std::vector non_orderable_list_info_formatter_outputs{ " }\n" "}\n", "json_info_empty"}, - {&json_formatter, &single_instance_info_reply, + {&json_formatter, + &single_instance_info_reply, "{\n" " \"errors\": [\n" " ],\n" @@ -819,12 +1615,219 @@ const std::vector non_orderable_list_info_formatter_outputs{ " }\n" " },\n" " \"release\": \"Ubuntu 16.04.3 LTS\",\n" + " \"snapshot_count\": \"0\",\n" + " \"state\": \"Running\"\n" + " }\n" + " }\n" + "}\n", + "json_info_single_instance"}, + {&json_formatter, + &multiple_instances_info_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"bogus-instance\": {\n" + " \"cpu_count\": \"4\",\n" + " \"disks\": {\n" + " \"sda1\": {\n" + " \"total\": \"6764573492\",\n" + " \"used\": \"1932735284\"\n" + " }\n" + " },\n" + " \"image_hash\": \"1797c5c82016c1e65f4008fcf89deae3a044ef76087a9ec5b907c6d64a3609ac\",\n" + " \"image_release\": \"16.04 LTS\",\n" + " \"ipv4\": [\n" + " \"10.21.124.56\"\n" + " ],\n" + " \"load\": [\n" + " 0.03,\n" + " 0.1,\n" + " 0.15\n" + " ],\n" + " \"memory\": {\n" + " \"total\": 1610612736,\n" + " \"used\": 38797312\n" + " },\n" + " \"mounts\": {\n" + " \"source\": {\n" + " \"gid_mappings\": [\n" + " \"1000:501\"\n" + " ],\n" + " \"source_path\": \"/home/user/source\",\n" + " \"uid_mappings\": [\n" + " \"1000:501\"\n" + " ]\n" + " }\n" + " },\n" + " \"release\": \"Ubuntu 16.04.3 LTS\",\n" + " \"snapshot_count\": \"1\",\n" " \"state\": \"Running\"\n" + " },\n" + " \"bombastic\": {\n" + " \"cpu_count\": \"\",\n" + " \"disks\": {\n" + " \"sda1\": {\n" + " }\n" + " },\n" + " \"image_hash\": \"ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509\",\n" + " \"image_release\": \"18.04 LTS\",\n" + " \"ipv4\": [\n" + " ],\n" + " \"load\": [\n" + " ],\n" + " \"memory\": {\n" + " },\n" + " \"mounts\": {\n" + " },\n" + " \"release\": \"\",\n" + " \"snapshot_count\": \"3\",\n" + " \"state\": \"Stopped\"\n" + " }\n" + " }\n" + "}\n", + "json_info_multiple_instances"}, + {&json_formatter, + &single_snapshot_info_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"bogus-instance\": {\n" + " \"snapshots\": {\n" + " \"snapshot2\": {\n" + " \"children\": [\n" + " \"snapshot3\",\n" + " \"snapshot4\"\n" + " ],\n" + " \"comment\": \"This is a comment with some\\nnew\\r\\nlines.\",\n" + " \"cpu_count\": \"2\",\n" + " \"created\": \"1972-01-01T10:00:20.021Z\",\n" + " \"disk_space\": \"4.9GiB\",\n" + " \"memory_size\": \"0.9GiB\",\n" + " \"mounts\": {\n" + " \"Home\": {\n" + " \"source_path\": \"/home/user\"\n" + " },\n" + " \"source\": {\n" + " \"source_path\": \"/home/user/source\"\n" + " }\n" + " },\n" + " \"parent\": \"snapshot1\",\n" + " \"size\": \"128MiB\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}\n", + "json_info_single_snapshot_info_reply"}, + {&json_formatter, + &multiple_snapshots_info_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"bogus-instance\": {\n" + " \"snapshots\": {\n" + " \"snapshot2\": {\n" + " \"children\": [\n" + " \"snapshot3\",\n" + " \"snapshot4\"\n" + " ],\n" + " \"comment\": \"\",\n" + " \"cpu_count\": \"2\",\n" + " \"created\": \"1972-01-01T10:00:20.021Z\",\n" + " \"disk_space\": \"4.9GiB\",\n" + " \"memory_size\": \"0.9GiB\",\n" + " \"mounts\": {\n" + " \"Home\": {\n" + " \"source_path\": \"/home/user\"\n" + " },\n" + " \"source\": {\n" + " \"source_path\": \"/home/user/source\"\n" + " }\n" + " },\n" + " \"parent\": \"snapshot1\",\n" + " \"size\": \"\"\n" + " }\n" + " }\n" + " },\n" + " \"messier-87\": {\n" + " \"snapshots\": {\n" + " \"black-hole\": {\n" + " \"children\": [\n" + " ],\n" + " \"comment\": \"Captured by EHT\",\n" + " \"cpu_count\": \"1\",\n" + " \"created\": \"2019-04-10T11:59:59Z\",\n" + " \"disk_space\": \"1024GiB\",\n" + " \"memory_size\": \"128GiB\",\n" + " \"mounts\": {\n" + " },\n" + " \"parent\": \"\",\n" + " \"size\": \"\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}\n", + "json_info_multiple_snapshots_info_reply"}, + {&json_formatter, + &mixed_instance_and_snapshot_info_reply, + "{\n" + " \"errors\": [\n" + " ],\n" + " \"info\": {\n" + " \"bogus-instance\": {\n" + " \"snapshots\": {\n" + " \"snapshot2\": {\n" + " \"children\": [\n" + " \"snapshot3\",\n" + " \"snapshot4\"\n" + " ],\n" + " \"comment\": \"\",\n" + " \"cpu_count\": \"2\",\n" + " \"created\": \"1972-01-01T10:00:20.021Z\",\n" + " \"disk_space\": \"4.9GiB\",\n" + " \"memory_size\": \"0.9GiB\",\n" + " \"mounts\": {\n" + " \"Home\": {\n" + " \"source_path\": \"/home/user\"\n" + " },\n" + " \"source\": {\n" + " \"source_path\": \"/home/user/source\"\n" + " }\n" + " },\n" + " \"parent\": \"snapshot1\",\n" + " \"size\": \"\"\n" + " }\n" + " }\n" + " },\n" + " \"bombastic\": {\n" + " \"cpu_count\": \"\",\n" + " \"disks\": {\n" + " \"sda1\": {\n" + " }\n" + " },\n" + " \"image_hash\": \"ab5191cc172564e7cc0eafd397312a32598823e645279c820f0935393aead509\",\n" + " \"image_release\": \"18.04 LTS\",\n" + " \"ipv4\": [\n" + " ],\n" + " \"load\": [\n" + " ],\n" + " \"memory\": {\n" + " },\n" + " \"mounts\": {\n" + " },\n" + " \"release\": \"\",\n" + " \"snapshot_count\": \"3\",\n" + " \"state\": \"Stopped\"\n" " }\n" " }\n" "}\n", - "json_info_single"}, - {&json_formatter, &multiple_instances_info_reply, + "json_info_mixed_instance_and_snapshot_info_reply"}, + {&json_formatter, + &multiple_mixed_instances_and_snapshots_info_reply, "{\n" " \"errors\": [\n" " ],\n" @@ -863,6 +1866,43 @@ const std::vector non_orderable_list_info_formatter_outputs{ " }\n" " },\n" " \"release\": \"Ubuntu 16.04.3 LTS\",\n" + " \"snapshot_count\": \"2\",\n" + " \"snapshots\": {\n" + " \"snapshot1\": {\n" + " \"children\": [\n" + " ],\n" + " \"comment\": \"\",\n" + " \"cpu_count\": \"2\",\n" + " \"created\": \"1972-01-01T09:59:59.021Z\",\n" + " \"disk_space\": \"4.9GiB\",\n" + " \"memory_size\": \"0.9GiB\",\n" + " \"mounts\": {\n" + " },\n" + " \"parent\": \"\",\n" + " \"size\": \"\"\n" + " },\n" + " \"snapshot2\": {\n" + " \"children\": [\n" + " \"snapshot3\",\n" + " \"snapshot4\"\n" + " ],\n" + " \"comment\": \"\",\n" + " \"cpu_count\": \"2\",\n" + " \"created\": \"1972-01-01T10:00:20.021Z\",\n" + " \"disk_space\": \"4.9GiB\",\n" + " \"memory_size\": \"0.9GiB\",\n" + " \"mounts\": {\n" + " \"Home\": {\n" + " \"source_path\": \"/home/user\"\n" + " },\n" + " \"source\": {\n" + " \"source_path\": \"/home/user/source\"\n" + " }\n" + " },\n" + " \"parent\": \"snapshot1\",\n" + " \"size\": \"\"\n" + " }\n" + " },\n" " \"state\": \"Running\"\n" " },\n" " \"bombastic\": {\n" @@ -882,55 +1922,82 @@ const std::vector non_orderable_list_info_formatter_outputs{ " \"mounts\": {\n" " },\n" " \"release\": \"\",\n" + " \"snapshot_count\": \"3\",\n" " \"state\": \"Stopped\"\n" + " },\n" + " \"messier-87\": {\n" + " \"snapshots\": {\n" + " \"black-hole\": {\n" + " \"children\": [\n" + " ],\n" + " \"comment\": \"Captured by EHT\",\n" + " \"cpu_count\": \"1\",\n" + " \"created\": \"2019-04-10T11:59:59Z\",\n" + " \"disk_space\": \"1024GiB\",\n" + " \"memory_size\": \"128GiB\",\n" + " \"mounts\": {\n" + " },\n" + " \"parent\": \"\",\n" + " \"size\": \"\"\n" + " }\n" + " }\n" " }\n" " }\n" "}\n", - "json_info_multiple"}}; + "json_info_multiple_mixed_instances_and_snapshots"}}; const std::vector non_orderable_networks_formatter_outputs{ {&table_formatter, &empty_networks_reply, "No network interfaces found.\n", "table_networks_empty"}, - {&table_formatter, &one_short_line_networks_reply, - "Name Type Description\n" - "en0 eth Ether\n", + {&table_formatter, + &one_short_line_networks_reply, + "Name Type Description\n" + "en0 eth Ether\n", "table_networks_one_short_line"}, - {&table_formatter, &one_long_line_networks_reply, - "Name Type Description\n" - "enp3s0 ethernet Amazingly fast and robust ethernet adapter\n", + {&table_formatter, + &one_long_line_networks_reply, + "Name Type Description\n" + "enp3s0 ethernet Amazingly fast and robust ethernet adapter\n", "table_networks_one_long_line"}, - {&table_formatter, &multiple_lines_networks_reply, - "Name Type Description\n" - "en0 eth Ether\n" - "wlx0123456789ab wifi Wireless\n", + {&table_formatter, + &multiple_lines_networks_reply, + "Name Type Description\n" + "en0 eth Ether\n" + "wlx0123456789ab wifi Wireless\n", "table_networks_multiple_lines"}, {&csv_formatter, &empty_networks_reply, "Name,Type,Description\n", "csv_networks_empty"}, - {&csv_formatter, &one_short_line_networks_reply, + {&csv_formatter, + &one_short_line_networks_reply, "Name,Type,Description\n" "en0,eth,\"Ether\"\n", "csv_networks_one_short_line"}, - {&csv_formatter, &one_long_line_networks_reply, + {&csv_formatter, + &one_long_line_networks_reply, "Name,Type,Description\n" "enp3s0,ethernet,\"Amazingly fast and robust ethernet adapter\"\n", "csv_networks_one_long_line"}, - {&csv_formatter, &multiple_lines_networks_reply, + {&csv_formatter, + &multiple_lines_networks_reply, "Name,Type,Description\n" "en0,eth,\"Ether\"\n" "wlx0123456789ab,wifi,\"Wireless\"\n", "csv_networks_multiple_lines"}, {&yaml_formatter, &empty_networks_reply, "\n", "yaml_networks_empty"}, - {&yaml_formatter, &one_short_line_networks_reply, + {&yaml_formatter, + &one_short_line_networks_reply, "en0:\n" " - type: eth\n" " description: Ether\n", "yaml_networks_one_short_line"}, - {&yaml_formatter, &one_long_line_networks_reply, + {&yaml_formatter, + &one_long_line_networks_reply, "enp3s0:\n" " - type: ethernet\n" " description: Amazingly fast and robust ethernet adapter\n", "yaml_networks_one_long_line"}, - {&yaml_formatter, &multiple_lines_networks_reply, + {&yaml_formatter, + &multiple_lines_networks_reply, "en0:\n" " - type: eth\n" " description: Ether\n" @@ -939,13 +2006,15 @@ const std::vector non_orderable_networks_formatter_outputs{ " description: Wireless\n", "yaml_networks_multiple_lines"}, - {&json_formatter, &empty_networks_reply, + {&json_formatter, + &empty_networks_reply, "{\n" " \"list\": [\n" " ]\n" "}\n", "json_networks_empty"}, - {&json_formatter, &one_short_line_networks_reply, + {&json_formatter, + &one_short_line_networks_reply, "{\n" " \"list\": [\n" " {\n" @@ -956,7 +2025,8 @@ const std::vector non_orderable_networks_formatter_outputs{ " ]\n" "}\n", "json_networks_one_short_line"}, - {&json_formatter, &one_long_line_networks_reply, + {&json_formatter, + &one_long_line_networks_reply, "{\n" " \"list\": [\n" " {\n" @@ -967,7 +2037,8 @@ const std::vector non_orderable_networks_formatter_outputs{ " ]\n" "}\n", "json_networks_one_long_line"}, - {&json_formatter, &multiple_lines_networks_reply, + {&json_formatter, + &multiple_lines_networks_reply, "{\n" " \"list\": [\n" " {\n" @@ -1366,6 +2437,12 @@ TEST_P(PetenvFormatterSuite, pet_env_first_in_output) if (auto input = dynamic_cast(reply)) { mp::ListReply reply_copy; + + if (input->has_instance_list()) + reply_copy.mutable_instance_list(); + else + reply_copy.mutable_snapshot_list(); + if (prepend) { add_petenv_to_reply(reply_copy); @@ -1379,9 +2456,9 @@ TEST_P(PetenvFormatterSuite, pet_env_first_in_output) output = formatter->format(reply_copy); if (dynamic_cast(formatter)) - regex = fmt::format("Name[[:print:]]*\n{}[[:space:]]+.*", petenv_name()); + regex = fmt::format("((Name|Instance)[[:print:]]*\n{0}[[:space:]]+.*)", petenv_name()); else if (dynamic_cast(formatter)) - regex = fmt::format("Name[[:print:]]*\n{},.*", petenv_name()); + regex = fmt::format("(Name|Instance)[[:print:]]*\n{},.*", petenv_name()); else if (dynamic_cast(formatter)) regex = fmt::format("{}:.*", petenv_name()); else @@ -1392,20 +2469,28 @@ TEST_P(PetenvFormatterSuite, pet_env_first_in_output) mp::InfoReply reply_copy; if (prepend) { - add_petenv_to_reply(reply_copy); + add_petenv_to_reply(reply_copy, + dynamic_cast(formatter), + test_name.find("snapshot") != std::string::npos); reply_copy.MergeFrom(*input); } else { reply_copy.CopyFrom(*input); - add_petenv_to_reply(reply_copy); + add_petenv_to_reply(reply_copy, + dynamic_cast(formatter), + test_name.find("snapshot") != std::string::npos); } output = formatter->format(reply_copy); if (dynamic_cast(formatter)) - regex = fmt::format("Name:[[:space:]]+{}.+", petenv_name()); + regex = fmt::format("(Name:[[:space:]]+{0}.+)" + "(Snapshot:[[:print:]]*\nInstance:[[:space:]]+{0}.+)", + petenv_name()); else if (dynamic_cast(formatter)) - regex = fmt::format("Name[[:print:]]*\n{},.*", petenv_name()); + regex = fmt::format("(Name[[:print:]]*\n{0},.*)|" + "(Snapshot[[:print:]]*\n[[:print:]]*,{0},.*)", + petenv_name()); else if (dynamic_cast(formatter)) regex = fmt::format("(errors:[[:space:]]+-[[:space:]]+~[[:space:]]+)?{}:.*", petenv_name()); else diff --git a/tests/test_qemuimg_process_spec.cpp b/tests/test_qemuimg_process_spec.cpp index 4a6f5ed95ae..2527f7b8a06 100644 --- a/tests/test_qemuimg_process_spec.cpp +++ b/tests/test_qemuimg_process_spec.cpp @@ -75,7 +75,7 @@ TEST(TestQemuImgProcessSpec, apparmor_profile_running_as_snap_correct) mp::QemuImgProcessSpec spec({}, source_image); EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1/usr/bin/qemu-img ixr,").arg(snap_dir.path()))); - EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rk,").arg(source_image))); + EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rwk,").arg(source_image))); } TEST(TestQemuImgProcessSpec, apparmor_profile_running_as_snap_with_target_correct) @@ -89,7 +89,7 @@ TEST(TestQemuImgProcessSpec, apparmor_profile_running_as_snap_with_target_correc mp::QemuImgProcessSpec spec({}, source_image, target_image); EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1/usr/bin/qemu-img ixr,").arg(snap_dir.path()))); - EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rk,").arg(source_image))); + EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rwk,").arg(source_image))); EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rwk,").arg(target_image))); } @@ -125,7 +125,7 @@ TEST(TestQemuImgProcessSpec, mp::QemuImgProcessSpec spec({}, source_image); EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1/usr/bin/qemu-img ixr,").arg(snap_dir.path()))); - EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rk,").arg(source_image))); + EXPECT_TRUE(spec.apparmor_profile().contains(QString("%1 rwk,").arg(source_image))); } TEST(TestQemuImgProcessSpec, apparmor_profile_not_running_as_snap_correct) diff --git a/tests/unix/test_daemon_rpc.cpp b/tests/unix/test_daemon_rpc.cpp index 33636866d89..b09201dfe89 100644 --- a/tests/unix/test_daemon_rpc.cpp +++ b/tests/unix/test_daemon_rpc.cpp @@ -79,6 +79,16 @@ struct TestDaemonRpc : public mpt::DaemonTestFixture return mpt::MockDaemon(config_builder.build()); } + void mock_empty_list_reply(mpt::MockDaemon& mock_daemon) + { + EXPECT_CALL(mock_daemon, list(_, _, _)).WillOnce([](auto, auto* server, auto* status_promise) { + mp::ListReply reply; + reply.mutable_instance_list(); + server->Write(reply); + status_promise->set_value(grpc::Status::OK); + }); + } + std::unique_ptr mock_cert_provider{std::make_unique()}; std::unique_ptr mock_cert_store{std::make_unique()}; @@ -202,9 +212,7 @@ TEST_F(TestDaemonRpc, listCertExistsCompletesSuccesfully) EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(true)); mpt::MockDaemon daemon{make_secure_server()}; - EXPECT_CALL(daemon, list(_, _, _)).WillOnce([](auto, auto, auto* status_promise) { - status_promise->set_value(grpc::Status::OK); - }); + mock_empty_list_reply(daemon); send_command({"list"}); } @@ -218,9 +226,7 @@ TEST_F(TestDaemonRpc, listNoCertsExistWillVerifyAndComplete) EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).Times(1); mpt::MockDaemon daemon{make_secure_server()}; - EXPECT_CALL(daemon, list(_, _, _)).WillOnce([](auto, auto, auto* status_promise) { - status_promise->set_value(grpc::Status::OK); - }); + mock_empty_list_reply(daemon); send_command({"list"}); } @@ -302,9 +308,7 @@ TEST_F(TestDaemonRpc, listSettingServerPermissionsFailLogsErrorAndExits) logger_scope.mock_logger->expect_log(mpl::Level::error, error_msg); logger_scope.mock_logger->expect_log(mpl::Level::error, "Failed to set up autostart prerequisites", AnyNumber()); - EXPECT_CALL(daemon, list(_, _, _)).WillOnce([](auto, auto, auto* status_promise) { - status_promise->set_value(grpc::Status::OK); - }); + mock_empty_list_reply(daemon); send_command({"list"}); }