Skip to content

Commit cb9b720

Browse files
committed
Revert "[hyperv-virtdisk] Remove virtdisk snapshot"
This reverts commit 421d99c.
1 parent ba2a8fc commit cb9b720

File tree

4 files changed

+397
-3
lines changed

4 files changed

+397
-3
lines changed

src/platform/backends/hyperv_api/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ if(WIN32)
6161
hcs/hyperv_hcs_path.cpp
6262
hcs/hyperv_hcs_api.cpp
6363
virtdisk/virtdisk_wrapper.cpp
64+
virtdisk/virtdisk_snapshot.cpp
6465
virtdisk/virtdisk_api.cpp
6566
hcs_virtual_machine.cpp
6667
hcs_virtual_machine_factory.cpp

src/platform/backends/hyperv_api/hcs_virtual_machine.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <hyperv_api/hcs/hyperv_hcs_event_type.h>
2424
#include <hyperv_api/hcs/hyperv_hcs_wrapper.h>
2525
#include <hyperv_api/hcs_virtual_machine_exceptions.h>
26+
#include <hyperv_api/virtdisk/virtdisk_snapshot.h>
2627
#include <hyperv_api/virtdisk/virtdisk_wrapper.h>
2728

2829
#include <shared/windows/smb_mount_handler.h>
@@ -279,7 +280,9 @@ bool HCSVirtualMachine::has_saved_state_file() const
279280
std::filesystem::path HCSVirtualMachine::get_primary_disk_path() const
280281
{
281282
const std::filesystem::path base_vhdx = description.image.image_path.toStdString();
282-
return base_vhdx;
283+
const std::filesystem::path head_avhdx =
284+
base_vhdx.parent_path() / virtdisk::VirtDiskSnapshot::head_disk_name();
285+
return MP_FILEOPS.exists(head_avhdx) ? head_avhdx : base_vhdx;
283286
}
284287

285288
void HCSVirtualMachine::grant_access_to_scsi_device(const hcs::HcsScsiDevice& device) const
@@ -686,6 +689,12 @@ void HCSVirtualMachine::resize_disk(const MemorySize& new_size)
686689
get_name(),
687690
new_size.in_megabytes());
688691

692+
if (get_num_snapshots() > 0)
693+
{
694+
throw ResizeDiskException{"Cannot resize the primary disk while there are "
695+
"snapshots. To resize, delete the snapshots first."};
696+
}
697+
689698
if (const auto result =
690699
VirtDisk().resize_virtual_disk(description.image.image_path.toStdString(),
691700
new_size.in_bytes());
@@ -743,12 +752,18 @@ std::shared_ptr<Snapshot> HCSVirtualMachine::make_specific_snapshot(
743752
const VMSpecs& specs,
744753
std::shared_ptr<Snapshot> parent)
745754
{
746-
throw NotImplementedOnThisBackendException{"snapshot"};
755+
return std::make_shared<virtdisk::VirtDiskSnapshot>(snapshot_name,
756+
comment,
757+
instance_id,
758+
parent,
759+
specs,
760+
*this,
761+
description);
747762
}
748763

749764
std::shared_ptr<Snapshot> HCSVirtualMachine::make_specific_snapshot(const QString& filename)
750765
{
751-
throw NotImplementedOnThisBackendException{"snapshot"};
766+
return std::make_shared<virtdisk::VirtDiskSnapshot>(filename, *this, description);
752767
}
753768

754769
} // namespace multipass::hyperv
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Copyright (C) Canonical, Ltd.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; version 3.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*
16+
*/
17+
18+
#include <hyperv_api/virtdisk/virtdisk_snapshot.h>
19+
20+
#include <hyperv_api/virtdisk/virtdisk_wrapper.h>
21+
22+
#include <multipass/exceptions/formatted_exception_base.h>
23+
#include <multipass/file_ops.h>
24+
#include <multipass/virtual_machine.h>
25+
#include <multipass/virtual_machine_description.h>
26+
27+
namespace
28+
{
29+
constexpr auto log_category = "virtdisk-snapshot";
30+
}
31+
32+
namespace multipass::hyperv::virtdisk
33+
{
34+
35+
struct CreateVirtdiskSnapshotError : FormattedExceptionBase<std::system_error>
36+
{
37+
using FormattedExceptionBase::FormattedExceptionBase;
38+
};
39+
40+
VirtDiskSnapshot::VirtDiskSnapshot(const std::string& name,
41+
const std::string& comment,
42+
const std::string& instance_id,
43+
std::shared_ptr<Snapshot> parent,
44+
const VMSpecs& specs,
45+
const VirtualMachine& vm,
46+
const VirtualMachineDescription& desc)
47+
: BaseSnapshot(name, comment, instance_id, std::move(parent), specs, vm),
48+
base_vhdx_path{desc.image.image_path.toStdString()},
49+
vm{vm}
50+
{
51+
}
52+
53+
VirtDiskSnapshot::VirtDiskSnapshot(const QString& filename,
54+
VirtualMachine& vm,
55+
const VirtualMachineDescription& desc)
56+
: BaseSnapshot(filename, vm, desc), base_vhdx_path{desc.image.image_path.toStdString()}, vm{vm}
57+
{
58+
}
59+
60+
std::string VirtDiskSnapshot::make_snapshot_filename(const Snapshot& ss)
61+
{
62+
constexpr auto snapshot_name_format = "{}.avhdx";
63+
return fmt::format(snapshot_name_format, ss.get_name());
64+
}
65+
66+
std::filesystem::path VirtDiskSnapshot::make_snapshot_path(const Snapshot& ss) const
67+
{
68+
return base_vhdx_path.parent_path() / make_snapshot_filename(ss);
69+
}
70+
71+
std::filesystem::path VirtDiskSnapshot::make_head_disk_path() const
72+
{
73+
return base_vhdx_path.parent_path() / head_disk_name();
74+
}
75+
76+
void VirtDiskSnapshot::capture_impl()
77+
{
78+
const auto& head_path = make_head_disk_path();
79+
const auto& snapshot_path = make_snapshot_path(*this);
80+
mpl::debug(log_category,
81+
"capture_impl() -> head_path: {}, snapshot_path: {}",
82+
head_path,
83+
snapshot_path);
84+
85+
// Check if head disk already exists. The head disk may not exist for a VM
86+
// that has no snapshots yet.
87+
if (!MP_FILEOPS.exists(head_path))
88+
{
89+
const auto& parent = get_parent();
90+
const auto& target = parent ? make_snapshot_path(*parent) : base_vhdx_path;
91+
create_new_child_disk(target, head_path);
92+
}
93+
94+
// Step 1: Rename current head to snapshot name
95+
MP_FILEOPS.rename(head_path, snapshot_path);
96+
97+
// Step 2: Create a new head from the snapshot
98+
create_new_child_disk(snapshot_path, head_path);
99+
}
100+
101+
void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent,
102+
const std::filesystem::path& child) const
103+
{
104+
mpl::debug(log_category, "create_new_child_disk() -> parent: {}, child: {}", parent, child);
105+
// The parent must already exist.
106+
if (!MP_FILEOPS.exists(parent))
107+
throw CreateVirtdiskSnapshotError{
108+
std::make_error_code(std::errc::no_such_file_or_directory),
109+
"Parent disk `{}` does not exist",
110+
parent};
111+
112+
// The given child path must not exist
113+
if (MP_FILEOPS.exists(child))
114+
throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists),
115+
"Child disk `{}` already exists",
116+
child};
117+
118+
const virtdisk::CreateVirtualDiskParameters params{.path = child,
119+
.predecessor =
120+
virtdisk::ParentPathParameters{parent}};
121+
122+
const auto result = VirtDisk().create_virtual_disk(params);
123+
if (result)
124+
{
125+
mpl::debug(log_category, "Successfully created the child disk: `{}`", child);
126+
return;
127+
}
128+
129+
throw CreateVirtdiskSnapshotError{
130+
result,
131+
"Could not create the head differencing disk for the snapshot"};
132+
}
133+
134+
void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots,
135+
const std::filesystem::path& new_parent) const
136+
{
137+
mpl::debug(log_category,
138+
"reparent_snapshot_disks() -> snapshots_count: {}, new_parent: {}",
139+
snapshots.size(),
140+
new_parent);
141+
142+
// The parent must already exist.
143+
if (!MP_FILEOPS.exists(new_parent))
144+
throw CreateVirtdiskSnapshotError{
145+
std::make_error_code(std::errc::no_such_file_or_directory),
146+
"Parent disk `{}` does not exist",
147+
new_parent};
148+
149+
for (const auto& child : snapshots)
150+
{
151+
const auto& child_path = make_snapshot_path(*child);
152+
153+
if (!MP_FILEOPS.exists(child_path))
154+
throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists),
155+
"Child disk `{}` does not exists",
156+
child_path};
157+
if (const auto result = VirtDisk().reparent_virtual_disk(child_path, new_parent); !result)
158+
{
159+
mpl::warn(log_category,
160+
"Could not reparent `{}` to `{}`: {}",
161+
child_path,
162+
new_parent,
163+
static_cast<std::error_code>(result));
164+
continue;
165+
}
166+
mpl::debug(log_category,
167+
"Successfully reparented the child disk `{}` to `{}`",
168+
child_path,
169+
new_parent);
170+
}
171+
}
172+
173+
void VirtDiskSnapshot::erase_impl()
174+
{
175+
176+
const auto& parent = get_parent();
177+
const auto& self_path = make_snapshot_path(*this);
178+
const auto& head_path = make_head_disk_path();
179+
mpl::debug(log_category,
180+
"erase_impl() -> parent: {}, self_path: {}",
181+
parent ? parent->get_name() : "<none>",
182+
self_path);
183+
184+
const auto& head_parent = [&]() {
185+
std::vector<std::filesystem::path> chain{};
186+
constexpr auto depth = 2;
187+
const auto list_r = VirtDisk().list_virtual_disk_chain(head_path, chain, depth);
188+
189+
if (chain.size() == depth)
190+
return chain[1];
191+
192+
throw CreateVirtdiskSnapshotError{list_r, "Could not determine head disk's parent"};
193+
}();
194+
195+
const bool should_merge_head = (self_path == head_parent) && vm.get_num_snapshots() == 1;
196+
const bool should_reparent_head = (self_path == head_parent) && vm.get_num_snapshots() > 1;
197+
198+
// If the head's parent is this, and this is the last snapshot,
199+
// we need to merge the head to the snapshot first.
200+
if (should_merge_head)
201+
{
202+
// Head is attached to the snapshot that's going to be deleted.
203+
if (const auto merge_r = VirtDisk().merge_virtual_disk_into_parent(head_path); merge_r)
204+
{
205+
mpl::debug(log_category,
206+
"Successfully merged head differencing disk `{}` to parent disk `{}`",
207+
head_path,
208+
self_path);
209+
mpl::debug(log_category, "Removing the merged head disk file: `{}`", head_path);
210+
MP_FILEOPS.remove(head_path);
211+
}
212+
else
213+
{
214+
throw CreateVirtdiskSnapshotError{
215+
merge_r,
216+
"Could not merge head differencing disk to the edge snapshot"};
217+
}
218+
}
219+
220+
// 1: Merge this to its parent
221+
if (const auto merge_r = VirtDisk().merge_virtual_disk_into_parent(self_path); merge_r)
222+
{
223+
const auto& parent_path = parent ? make_snapshot_path(*parent) : base_vhdx_path;
224+
mpl::debug(log_category,
225+
"Successfully merged differencing disk `{}` to parent disk `{}`",
226+
self_path,
227+
parent_path);
228+
229+
// The actual reparenting of the children needs to happen here. Reparenting is not a simple
230+
// "-> now this is your parent" like thing. The children include parent's metadata
231+
// calculated based on actual contents, and merging a child disk to parent updates its
232+
// parent's metadata, too. Hence, the reparenting operation not only needs to happen to the
233+
// orphaned children, but also to the existing children of the parent as well, so the
234+
// updated metadata of the parent could be reflected to the all.
235+
const auto children_to_reparent =
236+
vm.view_snapshots([&parent, this_index = this->get_index()](const Snapshot& ss) {
237+
return
238+
// Exclude self.
239+
(ss.get_index() != this_index) &&
240+
// set_parent() for the orphaned children happens before erase() call
241+
// so they're already adopted by the self's parent at this point.
242+
(parent ? (ss.get_parents_index() == parent->get_index())
243+
: ss.get_parents_index() == 0);
244+
});
245+
reparent_snapshot_disks(children_to_reparent, parent_path);
246+
247+
if (should_reparent_head)
248+
{
249+
if (const auto res = VirtDisk().reparent_virtual_disk(head_path, parent_path); res)
250+
{
251+
mpl::debug(log_category, "Reparented head {} to {}", head_path, parent_path);
252+
}
253+
else
254+
{
255+
throw CreateVirtdiskSnapshotError{
256+
res,
257+
"Could not reparent head differencing disk to the parent"};
258+
}
259+
}
260+
}
261+
else
262+
{
263+
throw CreateVirtdiskSnapshotError{merge_r, "Could not merge differencing disk to parent"};
264+
}
265+
// Finally, erase the merged disk.
266+
mpl::debug(log_category, "Removing snapshot file: `{}`", self_path);
267+
if (!MP_FILEOPS.remove(self_path))
268+
{
269+
mpl::error(log_category, "Failed removing snapshot file: `{}`!", self_path);
270+
}
271+
}
272+
273+
void VirtDiskSnapshot::apply_impl()
274+
{
275+
const auto& head_path = base_vhdx_path.parent_path() / head_disk_name();
276+
const auto& snapshot_path = make_snapshot_path(*this);
277+
278+
// Restoring a snapshot means we're discarding the head state.
279+
std::error_code ec{};
280+
MP_FILEOPS.remove(head_path, ec);
281+
mpl::debug(log_category, "apply_impl() -> {} remove: {}", head_path, ec);
282+
283+
// Create a new head from the snapshot
284+
create_new_child_disk(snapshot_path, head_path);
285+
}
286+
287+
} // namespace multipass::hyperv::virtdisk

0 commit comments

Comments
 (0)