master
cpp 397 lines 13 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCVhdVolume.cpp
8
9 Abstract:
10
11 Internal implementation for VHD-backed named volumes.
12
13 --*/
14
15 #include "precomp.h"
16 #include "DockerHTTPClient.h"
17 #include "OptionParser.h"
18 #include "WSLCVhdVolume.h"
19 #include "WSLCVirtualMachine.h"
20 #include "WSLCVolumeMetadata.h"
21 #include "WslCoreFilesystem.h"
22 #include "wslc_schema.h"
23
24 using namespace wsl::windows::common;
25 using wsl::shared::Localization;
26
27 namespace wsl::windows::service::wslc {
28
29 namespace {
30 constexpr auto c_sizeBytesOpt = "SizeBytes";
31 constexpr auto c_fixedOpt = "Fixed";
32 constexpr auto c_uidOpt = "Uid";
33 constexpr auto c_gidOpt = "Gid";
34
35 struct VhdVolumeOptions
36 {
37 ULONGLONG SizeBytes{};
38 bool Fixed{false};
39 std::optional<uint32_t> Uid;
40 std::optional<uint32_t> Gid;
41
42 static VhdVolumeOptions Parse(const std::map<std::string, std::string>& DriverOpts)
43 {
44 OptionParser parser(DriverOpts);
45 VhdVolumeOptions opts{};
46
47 opts.SizeBytes = parser.Required<ULONGLONG>(c_sizeBytesOpt);
48 THROW_HR_WITH_USER_ERROR_IF(
49 E_INVALIDARG, Localization::MessageWslcInvalidVolumeOption(c_sizeBytesOpt, DriverOpts.at(c_sizeBytesOpt)), opts.SizeBytes == 0);
50
51 opts.Fixed = parser.OptionalBool(c_fixedOpt).value_or(false);
52
53 // Uid and Gid must be supplied together — leaving one as the
54 // mkfs default (root) is a confusing footgun.
55 opts.Uid = parser.Optional<uint32_t>(c_uidOpt);
56 opts.Gid = parser.Optional<uint32_t>(c_gidOpt);
57 if (opts.Uid.has_value() != opts.Gid.has_value())
58 {
59 const auto* missing = opts.Uid.has_value() ? c_gidOpt : c_uidOpt;
60 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcMissingVolumeOption(missing));
61 }
62
63 parser.RejectUnknown();
64
65 return opts;
66 }
67 };
68
69 std::string GenerateName()
70 {
71 std::random_device rd;
72 std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned short> random(rd());
73
74 std::array<unsigned short, 32> randomBytes;
75 std::generate(randomBytes.begin(), randomBytes.end(), random);
76
77 std::string name;
78 name.reserve(randomBytes.size() * 2);
79 for (auto b : randomBytes)
80 {
81 std::format_to(std::back_inserter(name), "{:02x}", static_cast<BYTE>(b));
82 }
83
84 return name;
85 }
86
87 void RemoveLostFoundDirectory(WSLCVirtualMachine& VirtualMachine, const std::string& VolumeName, const std::string& MountPath)
88 try
89 {
90 constexpr auto c_lostFoundDir = "lost+found";
91 const auto entries = VirtualMachine.ListDirectory(MountPath);
92
93 // Only remove lost+found if the disk is empty besides that directory.
94 if (entries.size() != 1 || entries.front() != c_lostFoundDir)
95 {
96 return;
97 }
98
99 try
100 {
101 VirtualMachine.RemoveDirectory(std::format("{}/{}", MountPath, c_lostFoundDir));
102 }
103 catch (...)
104 {
105 // rmdir only removes an empty directory, so reaching here means the
106 // lone lost+found captured recovered data. Leave it and warn.
107 LOG_CAUGHT_EXCEPTION();
108 EMIT_USER_WARNING(Localization::MessageWslcVolumeLostFoundNotEmpty(VolumeName));
109 }
110 }
111 CATCH_LOG();
112 } // namespace
113
114 WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
115 std::string&& Name,
116 std::filesystem::path&& HostPath,
117 ULONGLONG SizeBytes,
118 ULONG Lun,
119 std::string&& VirtualMachinePath,
120 std::string&& CreatedAt,
121 std::string&& Mountpoint,
122 std::map<std::string, std::string>&& DriverOpts,
123 std::map<std::string, std::string>&& Labels,
124 WSLCVirtualMachine& VirtualMachine,
125 DockerHTTPClient& DockerClient,
126 bool Attached,
127 std::pair<HRESULT, std::string> Status) :
128 m_name(std::move(Name)),
129 m_hostPath(std::move(HostPath)),
130 m_virtualMachinePath(std::move(VirtualMachinePath)),
131 m_createdAt(std::move(CreatedAt)),
132 m_mountpoint(std::move(Mountpoint)),
133 m_driverOpts(std::move(DriverOpts)),
134 m_labels(std::move(Labels)),
135 m_sizeBytes(SizeBytes),
136 m_lun(Lun),
137 m_virtualMachine(VirtualMachine),
138 m_dockerClient(DockerClient),
139 m_attached(Attached),
140 m_status(std::move(Status))
141 {
142 }
143
144 WSLCVhdVolumeImpl::~WSLCVhdVolumeImpl()
145 {
146 Detach();
147 }
148
149 std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
150 LPCSTR Name,
151 std::map<std::string, std::string>&& DriverOpts,
152 std::map<std::string, std::string>&& Labels,
153 const std::filesystem::path& StoragePath,
154 WSLCVirtualMachine& VirtualMachine,
155 DockerHTTPClient& DockerClient)
156 {
157 std::string name = (Name != nullptr && Name[0] != '\0') ? std::string(Name) : GenerateName();
158 const auto opts = VhdVolumeOptions::Parse(DriverOpts);
159 auto hostPath = StoragePath / "volumes" / (name + ".vhdx");
160
161 auto createVhdCleanup =
162 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(hostPath.c_str())); });
163
164 std::filesystem::create_directories(hostPath.parent_path());
165
166 const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
167 wsl::core::filesystem::CreateVhd(hostPath.c_str(), opts.SizeBytes, tokenInfo->User.Sid, false, opts.Fixed);
168
169 auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
170 auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); });
171
172 // Ownership is baked into the ext4 root inode at format time so the
173 // container user can write without a post-mount chown.
174 VirtualMachine.Ext4Format(device, opts.Uid, opts.Gid);
175
176 auto virtualMachinePath = std::format("/mnt/wslc-volumes/{}", name);
177
178 // These should match the mount options used in Open
179 VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "discard", 0);
180
181 auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
182
183 // mkfs.ext4 always creates a lost+found directory at the filesystem root,
184 // which makes a freshly formatted volume look non-empty to Docker and
185 // suppresses the copy-up that seeds image data on first use. Drop it so
186 // Docker seeds the volume with the image's contents. No-op when the volume
187 // already contains data.
188 RemoveLostFoundDirectory(VirtualMachine, name, virtualMachinePath);
189
190 WSLCVolumeMetadata metadata;
191 metadata.Driver = WSLCVhdVolumeDriver;
192 metadata.DriverOpts = DriverOpts;
193 metadata.Properties = {
194 {"HostPath", hostPath.string()},
195 };
196
197 docker_schema::CreateVolume request{};
198 request.Name = name;
199 request.Driver = "local";
200 request.DriverOpts = {
201 {"type", "none"},
202 {"o", "bind"},
203 {"device", virtualMachinePath},
204 };
205 request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}};
206
207 // Merge user labels into the Docker volume labels.
208 for (const auto& [key, value] : Labels)
209 {
210 request.Labels[key] = value;
211 }
212
213 try
214 {
215 auto createdVolume = DockerClient.CreateVolume(request);
216
217 auto volume = std::make_unique<WSLCVhdVolumeImpl>(
218 std::move(name),
219 std::move(hostPath),
220 opts.SizeBytes,
221 lun,
222 std::move(virtualMachinePath),
223 std::move(createdVolume.CreatedAt),
224 std::move(createdVolume.Mountpoint),
225 std::move(DriverOpts),
226 std::move(Labels),
227 VirtualMachine,
228 DockerClient);
229
230 mountCleanup.release();
231 attachCleanup.release();
232 createVhdCleanup.release();
233
234 return volume;
235 }
236 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to create volume '%hs'", name.c_str());
237 }
238
239 std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
240 const wsl::windows::common::docker_schema::Volume& Volume, WSLCVirtualMachine& VirtualMachine, DockerHTTPClient& DockerClient)
241 {
242 THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value());
243
244 auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel);
245 THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end());
246
247 auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataIt->second.c_str());
248 THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCVhdVolumeDriver);
249
250 auto hostPathIt = metadata.Properties.find("HostPath");
251 THROW_HR_IF(E_INVALIDARG, hostPathIt == metadata.Properties.end());
252 THROW_HR_IF(E_INVALIDARG, hostPathIt->second.empty());
253
254 auto hostPath = std::filesystem::path(hostPathIt->second);
255 auto driverOpts = metadata.DriverOpts;
256 const auto opts = VhdVolumeOptions::Parse(driverOpts);
257
258 THROW_HR_IF(E_INVALIDARG, !Volume.Options.has_value());
259 auto deviceIt = Volume.Options->find("device");
260 THROW_HR_IF(E_INVALIDARG, deviceIt == Volume.Options->end());
261 THROW_HR_IF(E_INVALIDARG, deviceIt->second.empty());
262 std::string virtualMachinePath = deviceIt->second;
263
264 // Extract user labels (all labels except our internal metadata label).
265 std::map<std::string, std::string> userLabels;
266 for (const auto& [key, value] : *Volume.Labels)
267 {
268 if (key != WSLCVolumeMetadataLabel)
269 {
270 userLabels[key] = value;
271 }
272 }
273
274 ULONG lun = 0;
275 bool attached = false;
276 std::pair<HRESULT, std::string> status{S_OK, {}};
277
278 try
279 {
280 auto [attachedLun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
281 auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(attachedLun); });
282
283 // These should match the mount options used in Create
284 VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "discard", 0);
285 auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
286
287 RemoveLostFoundDirectory(VirtualMachine, Volume.Name, virtualMachinePath);
288
289 lun = attachedLun;
290 attached = true;
291
292 mountCleanup.release();
293 attachCleanup.release();
294 }
295 catch (...)
296 {
297 // The backing VHD could not be attached or mounted. Track the volume in an errored state so the user can still inspect
298 // and delete it; containers that reference it should refuse to start. The reason is surfaced via Inspect(), not the warning.
299 const auto hr = wil::ResultFromCaughtException();
300 const auto message = wslutil::GetErrorString(hr);
301 EMIT_USER_WARNING(Localization::MessageWslcFailedToRecoverVolume(Volume.Name));
302 status = {hr, wsl::shared::string::WideToMultiByte(message)};
303 }
304
305 return std::make_unique<WSLCVhdVolumeImpl>(
306 std::string{Volume.Name},
307 std::move(hostPath),
308 opts.SizeBytes,
309 lun,
310 std::move(virtualMachinePath),
311 std::string{Volume.CreatedAt},
312 std::string{Volume.Mountpoint},
313 std::move(driverOpts),
314 std::move(userLabels),
315 VirtualMachine,
316 DockerClient,
317 attached,
318 std::move(status));
319 }
320
321 void WSLCVhdVolumeImpl::Delete()
322 {
323 try
324 {
325 m_dockerClient.RemoveVolume(m_name);
326 }
327 catch (const DockerHTTPException& e)
328 {
329 THROW_HR_WITH_USER_ERROR_IF(
330 HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcVolumeInUse(m_name.c_str()), e.StatusCode() == 409);
331 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(m_name.c_str()), e.StatusCode() == 404);
332 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete volume '%hs'", m_name.c_str());
333 }
334
335 OnDeleted();
336 }
337
338 std::string WSLCVhdVolumeImpl::Inspect() const
339 {
340 wslc_schema::InspectVolume inspect{};
341 inspect.Name = m_name;
342 inspect.Driver = WSLCVhdVolumeDriver;
343 inspect.CreatedAt = m_createdAt;
344 inspect.Mountpoint = m_mountpoint;
345 inspect.Scope = WSLCVolumeScope;
346 inspect.Options = m_driverOpts;
347 inspect.Labels = m_labels;
348 inspect.Status = std::map<std::string, std::string>{
349 {"HostPath", m_hostPath.string()},
350 {"SizeBytes", std::to_string(m_sizeBytes)},
351 };
352
353 // Surface the recovery failure so callers can see why the volume is unusable.
354 if (FAILED(m_status.first))
355 {
356 inspect.Status->emplace("Error", m_status.second);
357 }
358
359 return wsl::shared::ToJson(inspect);
360 }
361
362 WSLCVolumeInformation WSLCVhdVolumeImpl::GetVolumeInformation() const
363 {
364 WSLCVolumeInformation Info{};
365
366 THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Name, m_name.c_str()) != 0);
367 THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Driver, WSLCVhdVolumeDriver) != 0);
368
369 return Info;
370 }
371
372 void WSLCVhdVolumeImpl::OnDeleted()
373 {
374 Detach();
375 LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_hostPath.c_str()));
376 }
377
378 void WSLCVhdVolumeImpl::Detach()
379 try
380 {
381 if (!m_attached)
382 {
383 return;
384 }
385
386 if (!m_virtualMachinePath.empty())
387 {
388 m_virtualMachine.Unmount(m_virtualMachinePath.c_str());
389 m_virtualMachinePath.clear();
390 }
391
392 m_virtualMachine.DetachDisk(m_lun);
393 m_attached = false;
394 }
395 CATCH_LOG();
396
397 } // namespace wsl::windows::service::wslc