Skip to content

Commit fcf58e6

Browse files
authored
[applevz] ASIF image support (#4676)
Add support for ASIF images on macOS 26 and later. Conversion from QCOW2 and existing VMs using RAW images are also handled. As the `qemu-img` binary is required by more than just the `qemu` backend, it has been factored out of the backend code into the `utils` directory. It can now be used by the `applevz` and `hyperv` backends without having the `qemu` backend be compiled. --- MULTI-2270 MULTI-2273
2 parents 5febddb + b50f03a commit fcf58e6

24 files changed

Lines changed: 761 additions & 165 deletions

include/multipass/json_utils.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ inline void tag_invoke(const boost::json::value_from_tag&,
167167

168168
inline QString tag_invoke(const boost::json::value_to_tag<QString>&, const boost::json::value& json)
169169
{
170-
return QString::fromStdString(value_to<std::string>(json));
170+
if (json.is_string())
171+
return QString::fromStdString(value_to<std::string>(json));
172+
return QString::fromStdString(boost::json::serialize(json));
171173
}
172174

173175
void tag_invoke(const boost::json::value_from_tag&,

src/platform/backends/qemu/qemu_img_utils.h renamed to include/multipass/utils/qemu_img_utils.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ class QemuImgException : public std::runtime_error
4040
Process::UPtr checked_exec_qemu_img(std::unique_ptr<QemuImgProcessSpec> spec,
4141
const std::string& custom_error_prefix = "Internal error",
4242
std::optional<int> timeout = std::nullopt);
43+
QString get_image_info(const std::filesystem::path& image_path, const QString& key);
4344
void resize_instance_image(const MemorySize& disk_space, const std::filesystem::path& image_path);
44-
std::filesystem::path convert_to_qcow_if_necessary(const std::filesystem::path& image_path);
45+
std::filesystem::path convert(const std::filesystem::path& image_path,
46+
const std::string& target_format);
4547
void amend_to_qcow2_v3(const std::filesystem::path& image_path);
46-
std::filesystem::path convert_to_raw(const std::filesystem::path& image_path);
4748
bool instance_image_has_snapshot(const std::filesystem::path& image_path, QString snapshot_tag);
4849
QByteArray snapshot_list_output(const std::filesystem::path& image_path);
4950
void delete_snapshot_from_image(const std::filesystem::path& image_path,

src/daemon/default_vm_image_vault.cpp

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <multipass/rpc/multipass.grpc.pb.h>
3232
#include <multipass/url_downloader.h>
3333
#include <multipass/utils.h>
34+
#include <multipass/utils/qemu_img_utils.h>
3435
#include <multipass/vm_image.h>
3536

3637
#include <QUrl>
@@ -85,37 +86,7 @@ void delete_image_dir(const mp::Path& image_path)
8586

8687
mp::MemorySize get_image_size(const std::filesystem::path& image_path)
8788
{
88-
QStringList qemuimg_parameters{{"info", MP_PLATFORM.path_to_qstr(image_path)}};
89-
auto qemuimg_process = mp::platform::make_process(
90-
std::make_unique<mp::QemuImgProcessSpec>(qemuimg_parameters, image_path));
91-
auto process_state = qemuimg_process->execute();
92-
93-
if (!process_state.completed_successfully())
94-
{
95-
throw std::runtime_error(
96-
fmt::format("Cannot get image info: qemu-img failed ({}) with output:\n{}",
97-
process_state.failure_message(),
98-
qemuimg_process->read_all_standard_error()));
99-
}
100-
101-
const auto img_info = QString{qemuimg_process->read_all_standard_output()};
102-
const auto pattern = QStringLiteral("^virtual size: .+ \\((?<size>\\d+) bytes\\)\r?$");
103-
const auto re = QRegularExpression{pattern, QRegularExpression::MultilineOption};
104-
105-
mp::MemorySize image_size{};
106-
107-
const auto match = re.match(img_info);
108-
109-
if (match.hasMatch())
110-
{
111-
image_size = mp::MemorySize(match.captured("size").toStdString());
112-
}
113-
else
114-
{
115-
throw std::runtime_error{"Could not obtain image's virtual size"};
116-
}
117-
118-
return image_size;
89+
return mp::MemorySize(mp::backend::get_image_info(image_path, "virtual-size").toStdString());
11990
}
12091

12192
void persist_records(const std::unordered_map<std::string, mp::VaultRecord>& records,

src/platform/backends/applevz/CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ add_library(applevz_backend STATIC
2020
applevz_virtual_machine_factory.cpp
2121
applevz_bridge.mm
2222
applevz_wrapper.cpp
23+
applevz_utils.mm
2324
)
2425

25-
set_source_files_properties(applevz_bridge.mm PROPERTIES
26+
set_source_files_properties(
27+
applevz_bridge.mm
28+
applevz_utils.mm
29+
PROPERTIES
2630
LANGUAGE OBJCXX
2731
COMPILE_FLAGS "-fobjc-arc"
2832
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
#pragma once
19+
20+
#include <multipass/memory_size.h>
21+
#include <multipass/singleton.h>
22+
23+
#include <filesystem>
24+
25+
#define MP_APPLEVZ_UTILS multipass::applevz::AppleVZUtils::instance()
26+
27+
namespace multipass::applevz
28+
{
29+
class AppleVZUtils : public Singleton<AppleVZUtils>
30+
{
31+
public:
32+
using Singleton<AppleVZUtils>::Singleton;
33+
34+
virtual std::filesystem::path convert_to_supported_format(
35+
const std::filesystem::path& image_path,
36+
bool destructive = true) const;
37+
virtual void resize_image(const MemorySize& disk_space,
38+
const std::filesystem::path& image_path) const;
39+
virtual bool macos_at_least(int major, int minor, int patch = 0) const;
40+
};
41+
} // namespace multipass::applevz
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
/*
19+
* This file provides some support for creating and resizing ASIF disk images.
20+
* There is no official public documentation for the ASIF format. The technical
21+
* details used here are based on community reverse engineering. For reference,
22+
* see: https://github.com/huven/asif-format
23+
*/
24+
25+
#include "applevz_utils.h"
26+
27+
#include <applevz/applevz_wrapper.h>
28+
#include <multipass/file_ops.h>
29+
#include <multipass/format.h>
30+
#include <multipass/logging/log.h>
31+
#include <multipass/utils/qemu_img_utils.h>
32+
#include <shared/macos/process_factory.h>
33+
34+
#include <Foundation/Foundation.h>
35+
36+
#include <scope_guard.hpp>
37+
38+
#include <algorithm>
39+
#include <array>
40+
#include <filesystem>
41+
#include <fstream>
42+
#include <stdexcept>
43+
#include <vector>
44+
45+
namespace mp = multipass;
46+
namespace mpl = multipass::logging;
47+
48+
namespace
49+
{
50+
constexpr auto category = "applevz-utils";
51+
52+
std::string run_process(const QString& program, const QStringList& args, const std::string& desc)
53+
{
54+
mpl::info(category, "Trying to {}", desc);
55+
56+
auto process = MP_PROCFACTORY.create_process(program, args);
57+
58+
if (const auto exit_state = process->execute(); !exit_state.completed_successfully())
59+
throw std::runtime_error(fmt::format("Failed to {}: {}; Output: {}",
60+
desc,
61+
exit_state.failure_message(),
62+
process->read_all_standard_error()));
63+
64+
return process->read_all_standard_output().trimmed().toStdString();
65+
}
66+
67+
bool is_asif_image(const std::filesystem::path& image_path)
68+
{
69+
// ASIF format uses "shdw" magic bytes (0x73686477)
70+
const auto file = MP_FILEOPS.open_read(image_path, std::ios::binary);
71+
file->exceptions(std::ios::badbit);
72+
if (file->fail())
73+
throw std::runtime_error(fmt::format("Failed to open file for reading: {}", image_path));
74+
75+
std::array<char, 4> magic{};
76+
file->read(magic.data(), magic.size());
77+
78+
return file->gcount() == 4 && std::equal(magic.begin(), magic.end(), "shdw");
79+
}
80+
81+
void resize_asif_image(const std::filesystem::path& image_path, const mp::MemorySize& disk_space)
82+
{
83+
run_process(
84+
QStringLiteral("diskutil"),
85+
QStringList() << "image" << "resize" << "--size" << QString::number(disk_space.in_bytes())
86+
<< MP_PLATFORM.path_to_qstr(image_path),
87+
fmt::format("resize ASIF image: {}, size: {}", image_path, disk_space.human_readable()));
88+
}
89+
90+
void create_asif_from(const std::filesystem::path& source_path,
91+
const std::filesystem::path& dest_path)
92+
{
93+
run_process(QStringLiteral("diskutil"),
94+
QStringList() << "image" << "create" << "from"
95+
<< MP_PLATFORM.path_to_qstr(source_path)
96+
<< MP_PLATFORM.path_to_qstr(dest_path) << "-f"
97+
<< "asif",
98+
fmt::format("convert {} to ASIF format at {}", source_path, dest_path));
99+
}
100+
101+
void make_sparse(const std::filesystem::path& raw_image_path, const mp::MemorySize& disk_space)
102+
{
103+
std::error_code ec;
104+
std::filesystem::resize_file(raw_image_path, disk_space.in_bytes(), ec);
105+
106+
if (ec)
107+
throw std::runtime_error(fmt::format("Failed to resize file: {}", ec.message()));
108+
}
109+
110+
std::filesystem::path convert_to_asif(const std::filesystem::path& source_path, bool destructive)
111+
{
112+
if (is_asif_image(source_path))
113+
return source_path;
114+
115+
mpl::info(category, "Converting {} to ASIF format", source_path);
116+
117+
// NO-OP if already RAW
118+
auto raw_path = mp::backend::convert(source_path, "raw");
119+
120+
// This is often an intermediate file so we remove it, unless it came from an existing VM
121+
auto intermediate_cleanup =
122+
sg::make_scope_guard([intermediate = (raw_path != source_path), raw_path]() noexcept {
123+
if (intermediate)
124+
QFile::remove(raw_path);
125+
});
126+
127+
auto asif_path = std::filesystem::path(source_path).replace_extension("asif");
128+
auto asif_file_cleanup =
129+
sg::make_scope_guard([&asif_path]() noexcept { QFile::remove(asif_path); });
130+
131+
create_asif_from(raw_path, asif_path);
132+
asif_file_cleanup.dismiss();
133+
134+
return asif_path;
135+
}
136+
} // namespace
137+
138+
namespace multipass::applevz
139+
{
140+
std::filesystem::path AppleVZUtils::convert_to_supported_format(
141+
const std::filesystem::path& image_path,
142+
bool destructive) const
143+
{
144+
return macos_at_least(26, 0) ? convert_to_asif(image_path, destructive)
145+
: backend::convert(image_path, "raw");
146+
}
147+
148+
void AppleVZUtils::resize_image(const MemorySize& disk_space,
149+
const std::filesystem::path& image_path) const
150+
{
151+
mpl::trace(category, "Resizing image to: {}", disk_space.human_readable());
152+
153+
is_asif_image(image_path) ? resize_asif_image(image_path, disk_space)
154+
: make_sparse(image_path, disk_space);
155+
156+
mpl::trace(category, "Successfully resized image: {}", image_path);
157+
}
158+
159+
bool AppleVZUtils::macos_at_least(int major, int minor, int patch) const
160+
{
161+
NSOperatingSystemVersion v{major, minor, patch};
162+
return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:v];
163+
}
164+
} // namespace multipass::applevz

src/platform/backends/applevz/applevz_virtual_machine.cpp

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
*
1616
*/
1717

18+
#include <applevz/applevz_utils.h>
1819
#include <applevz/applevz_virtual_machine.h>
19-
2020
#include <multipass/exceptions/internal_timeout_exception.h>
2121
#include <multipass/exceptions/virtual_machine_state_exceptions.h>
2222
#include <multipass/top_catch_all.h>
23+
#include <multipass/utils/qemu_img_utils.h>
2324
#include <multipass/vm_status_monitor.h>
24-
25-
#include <qemu/qemu_img_utils.h>
2625
#include <shared/macos/backend_utils.h>
2726

28-
namespace mpl = multipass::logging;
27+
namespace mp = multipass;
28+
namespace mpl = mp::logging;
2929

3030
namespace
3131
{
@@ -54,7 +54,7 @@ AppleVZVirtualMachine::~AppleVZVirtualMachine()
5454

5555
if (vm_handle)
5656
{
57-
multipass::top_catch_all(vm_name, [this]() {
57+
mp::top_catch_all(vm_name, [this]() {
5858
if (state == State::running)
5959
{
6060
suspend();
@@ -192,16 +192,16 @@ void AppleVZVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
192192
// We need to wait here.
193193
auto on_timeout = [] { throw std::runtime_error("timed out waiting for shutdown"); };
194194

195-
multipass::utils::try_action_for(on_timeout, std::chrono::seconds{180}, [this]() {
195+
mp::utils::try_action_for(on_timeout, std::chrono::seconds{180}, [this]() {
196196
switch (current_state())
197197
{
198198
case VirtualMachine::State::stopped:
199199
case VirtualMachine::State::off:
200200
drop_ssh_session();
201201
vm_handle.reset();
202-
return multipass::utils::TimeoutAction::done;
202+
return mp::utils::TimeoutAction::done;
203203
default:
204-
return multipass::utils::TimeoutAction::retry;
204+
return mp::utils::TimeoutAction::retry;
205205
}
206206
});
207207
}
@@ -271,7 +271,7 @@ std::string AppleVZVirtualMachine::ssh_username()
271271
std::optional<IPAddress> AppleVZVirtualMachine::management_ipv4()
272272
{
273273
if (!management_ip)
274-
management_ip = multipass::backend::get_neighbour_ip(desc.default_mac_address);
274+
management_ip = mp::backend::get_neighbour_ip(desc.default_mac_address);
275275

276276
return management_ip;
277277
}
@@ -296,7 +296,7 @@ void AppleVZVirtualMachine::resize_disk(const MemorySize& new_size)
296296
{
297297
assert(new_size > desc.disk_space);
298298

299-
multipass::backend::resize_instance_image(new_size, desc.image.image_path);
299+
MP_APPLEVZ_UTILS.resize_image(new_size, desc.image.image_path);
300300
desc.disk_space = new_size;
301301
}
302302

@@ -351,17 +351,17 @@ void AppleVZVirtualMachine::fetch_ip(std::chrono::milliseconds timeout)
351351

352352
auto action = [this] {
353353
detect_aborted_start();
354-
return ((management_ip = multipass::backend::get_neighbour_ip(desc.default_mac_address)))
355-
? multipass::utils::TimeoutAction::done
356-
: multipass::utils::TimeoutAction::retry;
354+
return ((management_ip = mp::backend::get_neighbour_ip(desc.default_mac_address)))
355+
? mp::utils::TimeoutAction::done
356+
: mp::utils::TimeoutAction::retry;
357357
};
358358

359359
auto on_timeout = [this, &timeout] {
360360
state = State::unknown;
361361
throw InternalTimeoutException{"determine IP address", timeout};
362362
};
363363

364-
multipass::utils::try_action_for(on_timeout, timeout, action);
364+
mp::utils::try_action_for(on_timeout, timeout, action);
365365
}
366366

367367
void AppleVZVirtualMachine::initialize_vm_handle()

0 commit comments

Comments
 (0)