diff --git a/include/multipass/snapshot.h b/include/multipass/snapshot.h index 4e764d7772a..c220e344829 100644 --- a/include/multipass/snapshot.h +++ b/include/multipass/snapshot.h @@ -40,7 +40,7 @@ class Snapshot : private DisabledCopyMove virtual std::string get_name() const = 0; virtual std::string get_comment() const = 0; - virtual std::string get_parent_name() const = 0; + virtual std::string get_parents_name() const = 0; virtual QDateTime get_creation_timestamp() const = 0; virtual std::shared_ptr get_parent() const = 0; virtual std::shared_ptr get_parent() = 0; diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index c8d7a451041..2b02b310981 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -96,6 +96,7 @@ class VirtualMachine : private DisabledCopyMove virtual void delete_snapshot(const QDir& snapshot_dir, const std::string& name) = 0; virtual void restore_snapshot(const QDir& snapshot_dir, const std::string& name, VMSpecs& specs) = 0; virtual void load_snapshots(const QDir& snapshot_dir) = 0; + virtual std::vector get_childrens_names(const Snapshot* parent) const = 0; VirtualMachine::State state; const std::string vm_name; diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index 63631bfa13d..89d2041df8b 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -1154,7 +1154,7 @@ bool is_ipv4_valid(const std::string& ipv4) struct SnapshotPick { std::unordered_set pick; - bool all; + bool all_or_none; }; using InstanceSnapshotPairs = google::protobuf::RepeatedPtrField; using InstanceSnapshotsMap = std::unordered_map; @@ -1169,7 +1169,7 @@ InstanceSnapshotsMap map_snapshots_to_instances(const InstanceSnapshotPairs& ins auto& snapshot_pick = instance_snapshots_map[instance]; if (snapshot.empty()) - snapshot_pick.all = true; + snapshot_pick.all_or_none = true; else snapshot_pick.pick.insert(snapshot); } @@ -1257,6 +1257,85 @@ bool prune_obsolete_mounts(const std::unordered_map& m 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_snapshot_overview(const std::string& instance_name, std::shared_ptr snapshot, + mp::SnapshotOverviewInfoItem* overview) +{ + auto fundamentals = overview->mutable_fundamentals(); + + overview->set_instance_name(instance_name); + populate_snapshot_fundamentals(snapshot, fundamentals); +} + +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::DetailedInfoItem* info, bool& have_mounts) +{ + 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 mp::Daemon::Daemon(std::unique_ptr the_config) @@ -1625,152 +1704,62 @@ try // clang-format on mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), *config->logger, server}; InfoReply response; + InstanceSnapshotsMap instance_snapshots_map; // Need to 'touch' a report in the response so formatters know what to do with an otherwise empty response request->snapshot_overview() ? (void)response.mutable_snapshot_overview() : (void)response.mutable_detailed_report(); bool have_mounts = false; bool deleted = false; - auto fetch_instance_info = [&](VirtualMachine& vm) { - const auto& name = vm.vm_name; - auto info = response.mutable_detailed_report()->add_details(); - auto instance_info = info->mutable_instance_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)); - } - - 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()) - { - 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(); - - mount_info->set_longest_path_len(0); + auto fetch_detailed_report = [&](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& [pick, all_or_none] = 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) - { - if (mount.second.source_path.size() > mount_info->longest_path_len()) - { - mount_info->set_longest_path_len(mount.second.source_path.size()); - } + if (all_or_none) + populate_instance_info(vm, response.mutable_detailed_report()->add_details(), + request->no_runtime_information(), deleted, have_mounts); - 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); - } - } + for (const auto& snapshot : pick) + populate_snapshot_info(vm, vm.get_snapshot(snapshot), response.mutable_detailed_report()->add_details(), + have_mounts); } - - if (!request->no_runtime_information() && mp::utils::is_running(present_state)) + catch (const NoSuchSnapshot& e) { - 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(); - 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); + add_fmt_to(errors, e.what()); } - return grpc::Status::OK; + + return grpc_status_for(errors); }; - InstanceSnapshotsMap instance_snapshots_map; auto fetch_snapshot_overview = [&](VirtualMachine& vm) { fmt::memory_buffer errors; const auto& name = vm.vm_name; - auto get_snapshot_info = [&](std::shared_ptr snapshot) { - auto overview = response.mutable_snapshot_overview()->add_overview(); - auto fundamentals = overview->mutable_fundamentals(); - - overview->set_instance_name(name); - fundamentals->set_snapshot_name(snapshot->get_name()); - fundamentals->set_parent(snapshot->get_parent_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); - }; - const auto& it = instance_snapshots_map.find(name); - const auto& [pick, all] = it == instance_snapshots_map.end() ? SnapshotPick{{}, true} : it->second; + const auto& [pick, all_or_none] = it == instance_snapshots_map.end() ? SnapshotPick{{}, true} : it->second; + auto overview = response.mutable_snapshot_overview()->add_overview(); try { - if (all) + if (all_or_none) { for (const auto& snapshot : pick) vm.get_snapshot(snapshot); // verify validity of any snapshot name requested separately for (const auto& snapshot : vm.view_snapshots()) - get_snapshot_info(snapshot); + populate_snapshot_overview(name, snapshot, overview); } else + { for (const auto& snapshot : pick) - get_snapshot_info(vm.get_snapshot(snapshot)); + populate_snapshot_overview(name, vm.get_snapshot(snapshot), overview); + } } catch (const NoSuchSnapshot& e) { @@ -1788,9 +1777,8 @@ try // clang-format on { instance_snapshots_map = map_snapshots_to_instances(request->instances_snapshots()); - // TODO@snapshots change cmd logic to include detailed snapshot info output - auto cmd = - request->snapshot_overview() ? std::function(fetch_snapshot_overview) : std::function(fetch_instance_info); + auto cmd = request->snapshot_overview() ? std::function(fetch_snapshot_overview) + : std::function(fetch_detailed_report); if ((status = cmd_vms(instance_selection.operative_selection, cmd)).ok()) { @@ -3362,3 +3350,71 @@ void mp::Daemon::reply_msg(grpc::ServerReaderWriterInterface* se server->Write(reply); } + +void mp::Daemon::populate_instance_info(VirtualMachine& vm, mp::DetailedInfoItem* info, bool no_runtime_info, + bool deleted, bool& have_mounts) +{ + const auto& name = vm.vm_name; + auto instance_info = info->mutable_instance_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)); + + 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()) + { + 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(); + 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 aa0ac007c5b..3fb071ea10f 100644 --- a/src/daemon/daemon.h +++ b/src/daemon/daemon.h @@ -191,6 +191,9 @@ public slots: template void reply_msg(grpc::ServerReaderWriterInterface* server, std::string&& msg, bool sticky = false); + void populate_instance_info(VirtualMachine& vm, DetailedInfoItem* info, bool runtime_info, bool deleted, + bool& have_mounts); + std::unique_ptr config; std::unordered_map vm_instance_specs; InstanceTable operative_instances; diff --git a/src/platform/backends/shared/base_snapshot.cpp b/src/platform/backends/shared/base_snapshot.cpp index 4bc58724845..939c2aebb03 100644 --- a/src/platform/backends/shared/base_snapshot.cpp +++ b/src/platform/backends/shared/base_snapshot.cpp @@ -152,7 +152,7 @@ QJsonObject multipass::BaseSnapshot::serialize() const snapshot.insert("disk_space", QString::number(disk_space.in_bytes())); snapshot.insert("state", static_cast(state)); snapshot.insert("metadata", metadata); - snapshot.insert("parent", QString::fromStdString(get_parent_name())); + snapshot.insert("parent", QString::fromStdString(get_parents_name())); // Extract mount serialization QJsonArray json_mounts; diff --git a/src/platform/backends/shared/base_snapshot.h b/src/platform/backends/shared/base_snapshot.h index 852e5aa6553..411c952c956 100644 --- a/src/platform/backends/shared/base_snapshot.h +++ b/src/platform/backends/shared/base_snapshot.h @@ -41,7 +41,7 @@ class BaseSnapshot : public Snapshot std::string get_name() const override; std::string get_comment() const override; QDateTime get_creation_timestamp() const override; - std::string get_parent_name() const override; + std::string get_parents_name() const override; std::shared_ptr get_parent() const override; std::shared_ptr get_parent() override; @@ -115,7 +115,7 @@ inline QDateTime multipass::BaseSnapshot::get_creation_timestamp() const return creation_timestamp; } -inline std::string multipass::BaseSnapshot::get_parent_name() const +inline std::string multipass::BaseSnapshot::get_parents_name() const { std::unique_lock lock{mutex}; auto par = parent; diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index b0a70e59bca..1a808d88e34 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -356,6 +356,17 @@ void BaseVirtualMachine::load_snapshots(const QDir& snapshot_dir) load_generic_snapshot_info(snapshot_dir); } +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(const QDir& snapshot_dir) { try @@ -376,7 +387,7 @@ template void BaseVirtualMachine::log_latest_snapshot(LockT lock) const { auto num_snapshots = static_cast(snapshots.size()); - auto parent_name = head_snapshot->get_parent_name(); + auto parent_name = head_snapshot->get_parents_name(); assert(num_snapshots <= snapshot_count && "can't have more snapshots than were ever taken"); @@ -424,7 +435,7 @@ void BaseVirtualMachine::load_snapshot(const QJsonObject& json) auto BaseVirtualMachine::make_head_file_rollback(const Path& head_path, QFile& head_file) const { - return sg::make_scope_guard([this, &head_path, &head_file, old_head = head_snapshot->get_parent_name(), + return sg::make_scope_guard([this, &head_path, &head_file, old_head = head_snapshot->get_parents_name(), existed = head_file.exists()]() noexcept { head_file_rollback_helper(head_path, head_file, old_head, existed); }); diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 543fa692335..85a487994b3 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -69,6 +69,7 @@ class BaseVirtualMachine : public VirtualMachine void delete_snapshot(const QDir& snapshot_dir, const std::string& name) override; void restore_snapshot(const QDir& snapshot_dir, const std::string& name, VMSpecs& specs) override; void load_snapshots(const QDir& snapshot_dir) override; + std::vector get_childrens_names(const Snapshot* parent) const override; protected: virtual std::shared_ptr make_specific_snapshot(const QJsonObject& json) = 0; diff --git a/tests/mock_virtual_machine.h b/tests/mock_virtual_machine.h index 01e0e359be2..2aa59ce4f9a 100644 --- a/tests/mock_virtual_machine.h +++ b/tests/mock_virtual_machine.h @@ -75,6 +75,7 @@ struct MockVirtualMachineT : public T MOCK_METHOD(void, delete_snapshot, (const QDir& snapshot_dir, const std::string& name), (override)); MOCK_METHOD(void, restore_snapshot, (const QDir& snapshot_dir, const std::string&, VMSpecs&), (override)); MOCK_METHOD(void, load_snapshots, (const QDir&), (override)); + MOCK_METHOD(std::vector, get_childrens_names, (const Snapshot*), (const, override)); }; using MockVirtualMachine = MockVirtualMachineT<>; diff --git a/tests/stub_snapshot.h b/tests/stub_snapshot.h index 015c7da8e22..4546d07c777 100644 --- a/tests/stub_snapshot.h +++ b/tests/stub_snapshot.h @@ -42,7 +42,7 @@ struct StubSnapshot : public Snapshot return QDateTime{}; } - std::string get_parent_name() const override + std::string get_parents_name() const override { return {}; } diff --git a/tests/stub_virtual_machine.h b/tests/stub_virtual_machine.h index e1b2eb47425..fd27a2b7d61 100644 --- a/tests/stub_virtual_machine.h +++ b/tests/stub_virtual_machine.h @@ -156,6 +156,11 @@ struct StubVirtualMachine final : public multipass::VirtualMachine { } + std::vector get_childrens_names(const Snapshot*) const override + { + return {}; + } + StubSnapshot snapshot; }; } // namespace test