Skip to content

Commit

Permalink
Merge pull request #3096 from canonical/restore-generic-snapshot
Browse files Browse the repository at this point in the history
[snapshots] Implement generic part of snapshot restoring
  • Loading branch information
sharder996 authored May 26, 2023
2 parents 71f64b2 + 0161464 commit 1cf8855
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 41 deletions.
1 change: 1 addition & 0 deletions include/multipass/snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Snapshot : private DisabledCopyMove

virtual void capture() = 0; // not using the constructor, we need snapshot objects for existing snapshots too
virtual void erase() = 0; // not using the destructor, we want snapshots to stick around when daemon quits
virtual void apply() = 0;
};
} // namespace multipass

Expand Down
1 change: 1 addition & 0 deletions include/multipass/virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class VirtualMachine : private DisabledCopyMove
virtual std::shared_ptr<const Snapshot> get_snapshot(const std::string& name) const = 0;
virtual std::shared_ptr<const Snapshot> take_snapshot(const QDir& snapshot_dir, const VMSpecs& specs,
const std::string& name, const std::string& comment) = 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;

VirtualMachine::State state;
Expand Down
11 changes: 5 additions & 6 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2522,25 +2522,24 @@ try
return status_promise->set_value(
grpc::Status{grpc::INVALID_ARGUMENT, "Multipass can only restore snapshots of stopped instances."});

const auto spec_it = vm_instance_specs.find(instance_name);
auto spec_it = vm_instance_specs.find(instance_name);
assert(spec_it != vm_instance_specs.end() && "missing instance specs");

const auto& vm_dir = instance_directory(instance_name, *config);
if (!request->destructive())
{
reply_msg(server, fmt::format("Taking snapshot before restoring {}", instance_name));

const auto snapshot = vm_ptr->take_snapshot(instance_directory(instance_name, *config), spec_it->second, "",
const auto snapshot = vm_ptr->take_snapshot(vm_dir, spec_it->second, "",
fmt::format("Before restoring {}", request->snapshot()));

reply_msg(server, fmt::format("Snapshot taken: {}.{}", instance_name, snapshot->get_name()),
/* sticky = */ true);
}

// TODO@snapshots replace placeholder implementation
reply_msg(server, "Restoring snapshot");
mpl::log(mpl::Level::debug, category, "Restore placeholder");

auto snapshot_name = request->snapshot();
vm_ptr->restore_snapshot(vm_dir, request->snapshot(), spec_it->second);
persist_instances();

server->Write(reply);
}
Expand Down
5 changes: 5 additions & 0 deletions src/platform/backends/qemu/qemu_snapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ void mp::QemuSnapshot::erase_impl() // TODO@snapshots
throw NotImplementedOnThisBackendException{"Snapshot erasing"};
}

void mp::QemuSnapshot::apply_impl() // TODO@snapshots
{
// TODO@snapshots implement
}

QString mp::QemuSnapshot::make_tag() const
{
return snapshot_template.arg(get_name().c_str());
Expand Down
1 change: 1 addition & 0 deletions src/platform/backends/qemu/qemu_snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class QemuSnapshot : public BaseSnapshot
protected:
void capture_impl() override;
void erase_impl() override;
void apply_impl() override;

private:
QString make_tag() const;
Expand Down
8 changes: 8 additions & 0 deletions src/platform/backends/shared/base_snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ class BaseSnapshot : public Snapshot

void capture() final;
void erase() final;
void apply() final;

protected:
virtual void capture_impl() = 0;
virtual void erase_impl() = 0;
virtual void apply_impl() = 0;

private:
struct InnerJsonTag
Expand Down Expand Up @@ -178,4 +180,10 @@ inline void multipass::BaseSnapshot::erase()
erase_impl();
}

inline void multipass::BaseSnapshot::apply()
{
const std::unique_lock lock{mutex};
apply_impl();
}

#endif // MULTIPASS_BASE_SNAPSHOT_H
154 changes: 120 additions & 34 deletions src/platform/backends/shared/base_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

#include "base_virtual_machine.h"
#include "daemon/vm_specs.h" // TODO@snapshots move this

#include <multipass/exceptions/file_not_found_exception.h>
#include <multipass/exceptions/snapshot_name_taken.h>
Expand Down Expand Up @@ -44,6 +45,7 @@ constexpr auto head_filename = "snapshot-head";
constexpr auto count_filename = "snapshot-count";
constexpr auto index_digits = 4; // these two go together
constexpr auto max_snapshots = 1000ull; // replace suffix with uz for size_t in C++23
constexpr auto yes_overwrite = true;
} // namespace

namespace multipass
Expand Down Expand Up @@ -109,6 +111,34 @@ std::shared_ptr<const Snapshot> BaseVirtualMachine::get_snapshot(const std::stri
return snapshots.at(name);
}

void BaseVirtualMachine::take_snapshot_rollback_helper(SnapshotMap::iterator it, std::shared_ptr<Snapshot>& old_head,
size_t old_count)
{
if (old_head != head_snapshot)
{
assert(it->second && "snapshot not created despite modified head");
if (snapshot_count > old_count) // snapshot was created
{
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<const Snapshot> BaseVirtualMachine::take_snapshot(const QDir& snapshot_dir, const VMSpecs& specs,
const std::string& name, const std::string& comment)
{
Expand All @@ -127,24 +157,7 @@ std::shared_ptr<const Snapshot> BaseVirtualMachine::take_snapshot(const QDir& sn
throw SnapshotNameTaken{vm_name, snapshot_name};
}

auto rollback_on_failure = sg::make_scope_guard(
[this, it = it, old_head = head_snapshot, old_count = snapshot_count]() mutable noexcept {
if (old_head != head_snapshot)
{
assert(it->second && "snapshot not created despite modified head");
if (snapshot_count > old_count) // snapshot was created
{
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 rollback_on_failure = make_take_snapshot_rollback(it);

auto ret = head_snapshot = it->second = make_specific_snapshot(snapshot_name, comment, head_snapshot, specs);
ret->capture();
Expand Down Expand Up @@ -236,6 +249,26 @@ void BaseVirtualMachine::load_snapshot(const QJsonObject& json)
}
}

auto BaseVirtualMachine::make_head_file_rollback(const QString& head_path, QFile& head_file) const
{
return sg::make_scope_guard([this, &head_path, &head_file, old_head = head_snapshot->get_parent_name(),
existed = head_file.exists()]() noexcept {
head_file_rollback_helper(head_path, head_file, old_head, existed);
});
}

void BaseVirtualMachine::head_file_rollback_helper(const QString& head_path, QFile& head_file,
const std::string& old_head, bool existed) const
{
// best effort, ignore returns
if (!existed)
head_file.remove();
else
top_catch_all(vm_name, [&head_path, &old_head] {
MP_UTILS.make_file_with_content(head_path.toStdString(), old_head, yes_overwrite);
});
}

void BaseVirtualMachine::persist_head_snapshot(const QDir& snapshot_dir) const
{
assert(head_snapshot);
Expand All @@ -245,7 +278,7 @@ void BaseVirtualMachine::persist_head_snapshot(const QDir& snapshot_dir) const
.arg(QString::fromStdString(head_snapshot->get_name()), snapshot_extension);

auto snapshot_path = snapshot_dir.filePath(snapshot_filename);
auto head_path = snapshot_dir.filePath(head_filename);
auto head_path = derive_head_path(snapshot_dir);
auto count_path = snapshot_dir.filePath(count_filename);

auto rollback_snapshot_file = sg::make_scope_guard([&snapshot_filename]() noexcept {
Expand All @@ -254,31 +287,84 @@ void BaseVirtualMachine::persist_head_snapshot(const QDir& snapshot_dir) const

mp::write_json(head_snapshot->serialize(), snapshot_path);

auto overwrite = true;
QFile head_file{head_path};

auto rollback_head_file =
sg::make_scope_guard([this, &head_path, &head_file, overwrite, old_head = head_snapshot->get_parent_name(),
existed = head_file.exists()]() noexcept {
// best effort, ignore returns
if (!existed)
head_file.remove();
else
mp::top_catch_all(vm_name, [&head_path, &old_head, overwrite] {
MP_UTILS.make_file_with_content(head_path.toStdString(), old_head, overwrite);
});
});
auto head_file_rollback = make_head_file_rollback(head_path, head_file);

MP_UTILS.make_file_with_content(head_path.toStdString(), head_snapshot->get_name(), overwrite);
MP_UTILS.make_file_with_content(count_path.toStdString(), std::to_string(snapshot_count), overwrite);
persist_head_snapshot_name(head_path);
MP_UTILS.make_file_with_content(count_path.toStdString(), std::to_string(snapshot_count), yes_overwrite);

rollback_snapshot_file.dismiss();
rollback_head_file.dismiss();
head_file_rollback.dismiss();
}

QString BaseVirtualMachine::derive_head_path(const QDir& snapshot_dir) const
{
return snapshot_dir.filePath(head_filename);
}

void BaseVirtualMachine::persist_head_snapshot_name(const QString& head_path) const
{
MP_UTILS.make_file_with_content(head_path.toStdString(), head_snapshot->get_name(), yes_overwrite);
}

std::string BaseVirtualMachine::generate_snapshot_name() const
{
return fmt::format("snapshot{}", snapshot_count + 1);
}

auto BaseVirtualMachine::make_restore_rollback(const QString& 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 QString& head_path, const std::shared_ptr<Snapshot>& 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_name(head_path);
}
}

void BaseVirtualMachine::restore_snapshot(const QDir& snapshot_dir, const std::string& name, VMSpecs& specs)
{
using St [[maybe_unused]] = VirtualMachine::State;

std::unique_lock lock{snapshot_mutex};
assert(state == St::off || state == St::stopped);

auto snapshot = snapshots.at(name); // TODO@snapshots convert out_of_range exception, here and `get_snapshot`

// TODO@snapshots convert into runtime_errors (persisted info could have been tampered with)
assert(specs.disk_space == snapshot->get_disk_space() && "resizing VMs with snapshots isn't yet supported");
assert(snapshot->get_state() == St::off || snapshot->get_state() == St::stopped);

snapshot->apply();

const auto head_path = derive_head_path(snapshot_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.mounts = snapshot->get_mounts();
specs.metadata = snapshot->get_metadata();

if (head_snapshot != snapshot)
{
head_snapshot = snapshot;
persist_head_snapshot_name(head_path);
}

rollback.dismiss();
}

} // namespace multipass
20 changes: 19 additions & 1 deletion src/platform/backends/shared/base_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class BaseVirtualMachine : public VirtualMachine
// derived classes is a big refactor
std::shared_ptr<const Snapshot> take_snapshot(const QDir& snapshot_dir, const VMSpecs& specs,
const std::string& name, const std::string& comment) override;
void restore_snapshot(const QDir& snapshot_dir, const std::string& name, VMSpecs& specs) override;
void load_snapshots(const QDir& snapshot_dir) override;

protected:
Expand All @@ -69,16 +70,33 @@ class BaseVirtualMachine : public VirtualMachine
const VMSpecs& specs) = 0;

private:
using SnapshotMap = std::unordered_map<std::string, std::shared_ptr<Snapshot>>;

template <typename LockT>
void log_latest_snapshot(LockT lock) const;

void load_generic_snapshot_info(const QDir& snapshot_dir);
void load_snapshot_from_file(const QString& filename);
void load_snapshot(const QJsonObject& json);

auto make_take_snapshot_rollback(SnapshotMap::iterator it);
void take_snapshot_rollback_helper(SnapshotMap::iterator it, std::shared_ptr<Snapshot>& old_head, size_t old_count);

auto make_head_file_rollback(const QString& head_path, QFile& head_file) const;
void head_file_rollback_helper(const QString& head_path, QFile& head_file, const std::string& old_head,
bool existed) const;
void persist_head_snapshot(const QDir& snapshot_dir) const;

void persist_head_snapshot_name(const QString& head_path) const;

QString derive_head_path(const QDir& snapshot_dir) const;
std::string generate_snapshot_name() const;

auto make_restore_rollback(const QString& head_path, VMSpecs& specs);
void restore_rollback_helper(const QString& head_path, const std::shared_ptr<Snapshot>& old_head,
const VMSpecs& old_specs, VMSpecs& specs);

private:
using SnapshotMap = std::unordered_map<std::string, std::shared_ptr<Snapshot>>;
SnapshotMap snapshots;
std::shared_ptr<Snapshot> head_snapshot = nullptr;
size_t snapshot_count = 0; // tracks the number of snapshots ever taken (regardless or deletes)
Expand Down
1 change: 1 addition & 0 deletions tests/mock_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ struct MockVirtualMachineT : public T
MOCK_METHOD(std::shared_ptr<const Snapshot>, get_snapshot, (const std::string&), (const, override));
MOCK_METHOD(std::shared_ptr<const Snapshot>, take_snapshot,
(const QDir&, const VMSpecs&, const std::string&, const std::string&), (override));
MOCK_METHOD(void, restore_snapshot, (const QDir& snapshot_dir, const std::string&, VMSpecs&), (override));
MOCK_METHOD(void, load_snapshots, (const QDir&), (override));
};

Expand Down
4 changes: 4 additions & 0 deletions tests/stub_snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ struct StubSnapshot : public Snapshot
{
}

void apply() override
{
}

std::unordered_map<std::string, VMMount> mounts;
QJsonObject metadata;
};
Expand Down
4 changes: 4 additions & 0 deletions tests/stub_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ struct StubVirtualMachine final : public multipass::VirtualMachine
return {};
}

void restore_snapshot(const QDir& snapshot_dir, const std::string& name, VMSpecs& specs) override
{
}

void load_snapshots(const QDir&) override
{
}
Expand Down

0 comments on commit 1cf8855

Please sign in to comment.