diff --git a/src/platform/backends/applevz/CMakeLists.txt b/src/platform/backends/applevz/CMakeLists.txt index 9eac297ccd..1025a153db 100644 --- a/src/platform/backends/applevz/CMakeLists.txt +++ b/src/platform/backends/applevz/CMakeLists.txt @@ -21,11 +21,13 @@ add_library(applevz_backend STATIC applevz_bridge.mm applevz_wrapper.cpp applevz_utils.mm + applevz_vmnet.mm ) set_source_files_properties( applevz_bridge.mm applevz_utils.mm + applevz_vmnet.mm PROPERTIES LANGUAGE OBJCXX COMPILE_FLAGS "-fobjc-arc" @@ -33,6 +35,7 @@ set_source_files_properties( find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) find_library(VIRTUALIZATION_FRAMEWORK Virtualization REQUIRED) +find_library(VMNET_FRAMEWORK vmnet REQUIRED) target_link_libraries(applevz_backend PUBLIC @@ -43,4 +46,5 @@ target_link_libraries(applevz_backend daemon ${FOUNDATION_FRAMEWORK} ${VIRTUALIZATION_FRAMEWORK} + ${VMNET_FRAMEWORK} Qt6::Core) diff --git a/src/platform/backends/applevz/applevz_bridge.h b/src/platform/backends/applevz/applevz_bridge.h index 9fb893efce..0d8ea37c38 100644 --- a/src/platform/backends/applevz/applevz_bridge.h +++ b/src/platform/backends/applevz/applevz_bridge.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -63,6 +64,9 @@ bool can_stop(const VMHandle& vm_handle); bool can_request_stop(const VMHandle& vm_handle); bool is_supported(); + +// Networking +std::vector bridged_network_interfaces(); } // namespace multipass::applevz template <> diff --git a/src/platform/backends/applevz/applevz_bridge.mm b/src/platform/backends/applevz/applevz_bridge.mm index c42ec486e1..97d90e2427 100644 --- a/src/platform/backends/applevz/applevz_bridge.mm +++ b/src/platform/backends/applevz/applevz_bridge.mm @@ -16,6 +16,7 @@ */ #include +#include #include #include @@ -29,13 +30,16 @@ #include #include +#include + namespace multipass::applevz { struct VirtualMachineHandle { - std::shared_ptr vm; // Ownership transfer of VZVirtualMachine* - dispatch_queue_t queue; // Dispatch queue for VM operations - uint64_t id; // Unique VM ID + std::shared_ptr vm; // Ownership transfer of VZVirtualMachine* + dispatch_queue_t queue; // Dispatch queue for VM operations + uint64_t id; // Unique VM ID + std::vector relays; // vmnet relay lifetime handles }; } // namespace multipass::applevz @@ -172,18 +176,40 @@ CFError init_with_configuration(const multipass::VirtualMachineDescription& desc [[VZVirtioEntropyDeviceConfiguration alloc] init]; config.entropyDevices = @[ entropy ]; - // Network device + // Network devices + // Primary NAT interface + NSMutableArray* networkDevices = [NSMutableArray array]; + VZVirtioNetworkDeviceConfiguration* netDevice = [[VZVirtioNetworkDeviceConfiguration alloc] init]; - VZNATNetworkDeviceAttachment* natAttachment = [[VZNATNetworkDeviceAttachment alloc] init]; netDevice.attachment = natAttachment; VZMACAddress* mac = [[VZMACAddress alloc] initWithString:nsstring_from_stdstring(desc.default_mac_address)]; [netDevice setMACAddress:mac]; + [networkDevices addObject:netDevice]; + + // Extra bridged interfaces + std::vector relays; + for (const auto& extra : desc.extra_interfaces) + { + VmnetBridge bridge = create_vmnet_bridge(extra.id); + + VZVirtioNetworkDeviceConfiguration* bridgedDevice = + [[VZVirtioNetworkDeviceConfiguration alloc] init]; + bridgedDevice.attachment = bridge.attachment; + VZMACAddress* bridgedMac = [[VZMACAddress alloc] + initWithString:[NSString stringWithCString:extra.mac_address.c_str() + encoding:NSUTF8StringEncoding]]; + [bridgedDevice setMACAddress:bridgedMac]; + + [networkDevices addObject:bridgedDevice]; + + relays.push_back(std::move(bridge.relay)); + } - config.networkDevices = @[ netDevice ]; + config.networkDevices = networkDevices; // Memory balloon device VZVirtioTraditionalMemoryBalloonDeviceConfiguration* balloon = @@ -197,7 +223,7 @@ CFError init_with_configuration(const multipass::VirtualMachineDescription& desc } out_handle = std::make_shared(); - + out_handle->relays = std::move(relays); out_handle->id = vmIDCounter.fetch_add(1, std::memory_order_relaxed); // Create dispatch queue @@ -306,4 +332,20 @@ bool is_supported() { return [VZVirtualMachine isSupported]; } + +std::vector bridged_network_interfaces() +{ + std::vector result; + + for (VZBridgedNetworkInterface* iface in [VZBridgedNetworkInterface networkInterfaces]) + { + result.emplace_back(/* id = */ iface.identifier.UTF8String, + /* type = */ "N/A", + /* description = */ iface.localizedDisplayName + ? std::string(iface.localizedDisplayName.UTF8String) + : iface.identifier.UTF8String); + } + + return result; +} } // namespace multipass::applevz diff --git a/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp b/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp index a32846c018..2416c61eb1 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp +++ b/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp @@ -70,6 +70,16 @@ void AppleVZVirtualMachineFactory::remove_resources_for_impl(const std::string& { } +std::vector AppleVZVirtualMachineFactory::networks() const +{ + return MP_APPLEVZ.bridged_network_interfaces(); +} + +std::string AppleVZVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& interface) +{ + return interface.id; +} + VirtualMachine::UPtr AppleVZVirtualMachineFactory::clone_vm_impl( const std::string& /*source_vm_name*/, const multipass::VMSpecs& /*src_vm_specs*/, diff --git a/src/platform/backends/applevz/applevz_virtual_machine_factory.h b/src/platform/backends/applevz/applevz_virtual_machine_factory.h index 79e5fcbc3b..5afffefe60 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine_factory.h +++ b/src/platform/backends/applevz/applevz_virtual_machine_factory.h @@ -48,8 +48,11 @@ class AppleVZVirtualMachineFactory final : public BaseVirtualMachineFactory return "applevz"; }; + std::vector networks() const override; + protected: void remove_resources_for_impl(const std::string& name) override; + std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; private: VirtualMachine::UPtr clone_vm_impl(const std::string& source_vm_name, diff --git a/src/platform/backends/applevz/applevz_vmnet.h b/src/platform/backends/applevz/applevz_vmnet.h new file mode 100644 index 0000000000..7a23fc100e --- /dev/null +++ b/src/platform/backends/applevz/applevz_vmnet.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::applevz +{ + +// Opaque handle whose lifetime must exceed that of the attachment it was created with. +using VmnetRelayHandle = std::shared_ptr; + +struct VmnetBridge +{ + VZFileHandleNetworkDeviceAttachment* attachment; + VmnetRelayHandle relay; +}; + +VmnetBridge create_vmnet_bridge(const std::string& physical_iface); + +} // namespace multipass::applevz diff --git a/src/platform/backends/applevz/applevz_vmnet.mm b/src/platform/backends/applevz/applevz_vmnet.mm new file mode 100644 index 0000000000..04c006091b --- /dev/null +++ b/src/platform/backends/applevz/applevz_vmnet.mm @@ -0,0 +1,372 @@ +/* + * Copyright (C) The vmnet-helper authors + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/* + Portions of this file are derived from vmnet-helper by Nir Soffer + (https://github.com/nirs/vmnet-helper), originally licensed under the + Apache License, Version 2.0. The combined work is distributed under + the terms of GPL-3.0. + + The following adaptations were made for use with Multipass: + - rewritten in idiomatic Objective-C++ and integrated as a library; + - connected to Apple Virtualization framework via a socketpair and + VZFileHandleNetworkDeviceAttachment; + - removed unused network options + - Multipass logging; + */ + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace mpl = multipass::logging; + +namespace +{ +constexpr auto category = "vmnet"; +constexpr uint32_t kSendBufferSize{65 * 1024}; +constexpr uint32_t kRecvBufferSize{4 * 1024 * 1024}; +constexpr int kMaxPacketCount{200}; +constexpr int kMaxPacketCountLarge{16}; +constexpr uint32_t kLargePacketThreshold{64 * 1024}; +constexpr std::chrono::microseconds kSendRetryDelay{50}; + +struct msghdr_x +{ + struct msghdr msg_hdr; + size_t msg_len; +}; +extern "C" ssize_t recvmsg_x(int s, const msghdr_x* msgp, unsigned int cnt, int flags); +extern "C" ssize_t sendmsg_x(int s, const msghdr_x* msgp, unsigned int cnt, int flags); + +struct VmnetRelay +{ + interface_ref iface{nullptr}; + int fd{-1}; + dispatch_queue_t queue{nullptr}; + dispatch_queue_t vm_queue{nullptr}; + uint32_t max_packet_bytes{0}; + + struct EndpointBuffers + { + std::vector buffers; + std::vector packets; + std::vector iovs; + std::vector msgs; + + void bind_endpoint(int max_packet_count, uint32_t max_packet_bytes) + { + buffers.resize((size_t)max_packet_count * max_packet_bytes); + packets.resize(max_packet_count); + iovs.resize(max_packet_count); + msgs.resize(max_packet_count); + + for (int i = 0; i < max_packet_count; i++) + { + iovs[i].iov_base = buffers.data() + (size_t)i * max_packet_bytes; + iovs[i].iov_len = max_packet_bytes; + packets[i].vm_pkt_iov = &iovs[i]; + packets[i].vm_pkt_iovcnt = 1; + msgs[i].msg_hdr.msg_iov = &iovs[i]; + msgs[i].msg_hdr.msg_iovlen = 1; + } + } + }; + + // Pre-allocated buffers for host->vm (vmnet read / socket write). + EndpointBuffers host_endpoint; + // Pre-allocated buffers for vm->host (socket read / vmnet write). + EndpointBuffers vm_endpoint; + + void init_buffers() + { + const int packet_count = + (max_packet_bytes >= kLargePacketThreshold) ? kMaxPacketCountLarge : kMaxPacketCount; + + host_endpoint.bind_endpoint(packet_count, max_packet_bytes); + vm_endpoint.bind_endpoint(packet_count, max_packet_bytes); + } + + ~VmnetRelay() + { + if (fd >= 0) + close(fd); + + // Wait for the vm->host forwarding loop to exit before stopping vmnet. + if (vm_queue) + dispatch_sync(vm_queue, + ^{ + }); + + if (iface) + { + dispatch_semaphore_t s = dispatch_semaphore_create(0); + vmnet_stop_interface(iface, + dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), + ^(vmnet_return_t status) { + if (status != VMNET_SUCCESS) + mpl::warn(category, + "vmnet_stop_interface() failed (status {})", + static_cast(status)); + dispatch_semaphore_signal(s); + }); + if (dispatch_semaphore_wait(s, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)) != 0) + mpl::warn(category, "timed out waiting for vmnet_stop_interface() to complete"); + } + } + + VmnetRelay() = default; + VmnetRelay(const VmnetRelay&) = delete; + VmnetRelay& operator=(const VmnetRelay&) = delete; +}; + +void start_vmnet_interface(VmnetRelay& relay, const std::string& physical_iface) +{ + relay.queue = dispatch_queue_create( + fmt::format("com.canonical.multipassd.vmnet.{}", physical_iface).c_str(), + DISPATCH_QUEUE_SERIAL); + + xpc_object_t iface_opts = xpc_dictionary_create(nullptr, nullptr, 0); + xpc_dictionary_set_uint64(iface_opts, vmnet_operation_mode_key, VMNET_BRIDGED_MODE); + xpc_dictionary_set_string(iface_opts, vmnet_shared_interface_name_key, physical_iface.c_str()); + + dispatch_semaphore_t vmnet_sema = dispatch_semaphore_create(0); + __block vmnet_return_t vmnet_status = VMNET_FAILURE; + + // Starting the vmnet interface requires root on macOS 15 and older + relay.iface = vmnet_start_interface( + iface_opts, + relay.queue, + ^(vmnet_return_t status, xpc_object_t params) { + vmnet_status = status; + if (status == VMNET_SUCCESS) + relay.max_packet_bytes = + (uint32_t)xpc_dictionary_get_uint64(params, vmnet_max_packet_size_key); + dispatch_semaphore_signal(vmnet_sema); + }); + + dispatch_semaphore_wait(vmnet_sema, DISPATCH_TIME_FOREVER); + + if (vmnet_status != VMNET_SUCCESS || !relay.iface) + { + throw std::runtime_error(fmt::format("vmnet_start_interface() failed (status {}) for '{}'", + static_cast(vmnet_status), + physical_iface)); + } + + mpl::debug(category, "vmnet bridged interface started on '{}'", physical_iface); +} + +// Forward one batch of packets from the socket to the vmnet interface. +// Returns false when the socket is closed. +bool forward_from_vm(VmnetRelay& relay, bool bulk) +{ + int count = 0; + + if (bulk) + { + int max_count = (int)relay.vm_endpoint.msgs.size(); + for (int i = 0; i < max_count; i++) + relay.vm_endpoint.iovs[i].iov_len = relay.max_packet_bytes; + + count = (int)recvmsg_x(relay.fd, relay.vm_endpoint.msgs.data(), max_count, 0); + if (count < 0 && errno != EBADF && errno != ECONNRESET) + mpl::trace(category, "recvmsg_x() failed: {}", strerror(errno)); + } + else + { + relay.vm_endpoint.iovs[0].iov_len = relay.max_packet_bytes; + ssize_t n = recv(relay.fd, relay.vm_endpoint.iovs[0].iov_base, relay.max_packet_bytes, 0); + if (n < 0 && errno != EBADF && errno != ECONNRESET) + mpl::trace(category, "recv() failed: {}", strerror(errno)); + if (n > 0) + { + relay.vm_endpoint.msgs[0].msg_len = (size_t)n; + count = 1; + } + } + + if (count <= 0) + return false; + + for (int i = 0; i < count; i++) + { + relay.vm_endpoint.packets[i].vm_pkt_size = relay.vm_endpoint.msgs[i].msg_len; + relay.vm_endpoint.packets[i].vm_flags = 0; + relay.vm_endpoint.iovs[i].iov_len = relay.vm_endpoint.msgs[i].msg_len; + } + + int write_count = count; + vmnet_return_t status = + vmnet_write(relay.iface, relay.vm_endpoint.packets.data(), &write_count); + if (status != VMNET_SUCCESS) + mpl::trace(category, "vmnet_write() failed (status {})", static_cast(status)); + + return true; +} + +// Forward one batch of packets from the vmnet interface to the socket. +// Returns false when vmnet has no more packets to deliver this callback. +bool forward_from_host(VmnetRelay& relay, bool bulk) +{ + int count = (int)relay.host_endpoint.packets.size(); + for (int i = 0; i < count; i++) + { + relay.host_endpoint.packets[i].vm_pkt_size = relay.max_packet_bytes; + relay.host_endpoint.packets[i].vm_flags = 0; + relay.host_endpoint.iovs[i].iov_len = relay.max_packet_bytes; + } + + vmnet_return_t read_status = + vmnet_read(relay.iface, relay.host_endpoint.packets.data(), &count); + if (read_status != VMNET_SUCCESS) + { + mpl::trace(category, "vmnet_read() failed (status {})", static_cast(read_status)); + return false; + } + + if (bulk) + { + for (int i = 0; i < count; i++) + { + relay.host_endpoint.msgs[i].msg_len = relay.host_endpoint.packets[i].vm_pkt_size; + relay.host_endpoint.iovs[i].iov_len = relay.host_endpoint.packets[i].vm_pkt_size; + } + + int sent = 0; + while (sent < count) + { + ssize_t n = + sendmsg_x(relay.fd, relay.host_endpoint.msgs.data() + sent, count - sent, 0); + if (n < 0) + { + if (errno == ENOBUFS) + { + std::this_thread::sleep_for(kSendRetryDelay); + continue; + } + mpl::trace(category, "sendmsg_x() failed: {}", strerror(errno)); + break; + } + sent += (int)n; + } + } + else + { + for (int i = 0; i < count; i++) + { + const auto* data = static_cast(relay.host_endpoint.iovs[i].iov_base); + size_t pkt_size = relay.host_endpoint.packets[i].vm_pkt_size; + while (send(relay.fd, data, pkt_size, 0) < 0) + { + if (errno == ENOBUFS) + std::this_thread::sleep_for(kSendRetryDelay); + else + { + mpl::trace(category, "send() failed: {}", strerror(errno)); + break; + } + } + } + } + + return count > 0; +} + +void start_forwarding_from_vm(VmnetRelay& relay) +{ + const bool bulk = MP_APPLEVZ_UTILS.macos_at_least(14, 0); + + relay.vm_queue = + dispatch_queue_create("com.canonical.multipassd.vmnet.vm", DISPATCH_QUEUE_SERIAL); + dispatch_async(relay.vm_queue, ^{ + while (forward_from_vm(relay, bulk)) + ; + }); + + mpl::debug(category, "vmnet vm->host forwarding started"); +} + +void start_forwarding_from_host(VmnetRelay& relay) +{ + const bool bulk = MP_APPLEVZ_UTILS.macos_at_least(14, 0); + + vmnet_return_t status = + vmnet_interface_set_event_callback(relay.iface, + VMNET_INTERFACE_PACKETS_AVAILABLE, + relay.queue, + ^(interface_event_t /*event*/, xpc_object_t /*params*/) { + while (forward_from_host(relay, bulk)) + ; + }); + + if (status != VMNET_SUCCESS) + throw std::runtime_error( + fmt::format("vmnet_interface_set_event_callback() failed (status {})", + static_cast(status))); + + mpl::debug(category, "vmnet host->vm forwarding started"); +} + +std::array create_socket_pair() +{ + std::array fds{}; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fds.data()) < 0) + throw std::runtime_error(fmt::format("socketpair() failed: {}", strerror(errno))); + + setsockopt(fds[0], SOL_SOCKET, SO_RCVBUF, &kRecvBufferSize, sizeof(kRecvBufferSize)); + setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &kSendBufferSize, sizeof(kSendBufferSize)); + setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &kRecvBufferSize, sizeof(kRecvBufferSize)); + setsockopt(fds[1], SOL_SOCKET, SO_SNDBUF, &kSendBufferSize, sizeof(kSendBufferSize)); + + return fds; +} +} // namespace + +namespace multipass::applevz +{ +VmnetBridge create_vmnet_bridge(const std::string& physical_iface) +{ + auto relay = std::make_unique(); + start_vmnet_interface(*relay, physical_iface); + + auto [vz_fd, relay_fd] = create_socket_pair(); + + relay->fd = relay_fd; + relay->init_buffers(); + + start_forwarding_from_host(*relay); + start_forwarding_from_vm(*relay); + + NSFileHandle* vz_handle = [[NSFileHandle alloc] initWithFileDescriptor:vz_fd + closeOnDealloc:YES]; + VZFileHandleNetworkDeviceAttachment* attachment = + [[VZFileHandleNetworkDeviceAttachment alloc] initWithFileHandle:vz_handle]; + + return VmnetBridge{attachment, std::move(relay)}; +} +} // namespace multipass::applevz diff --git a/src/platform/backends/applevz/applevz_wrapper.cpp b/src/platform/backends/applevz/applevz_wrapper.cpp index b5cf003d03..debcfd800f 100644 --- a/src/platform/backends/applevz/applevz_wrapper.cpp +++ b/src/platform/backends/applevz/applevz_wrapper.cpp @@ -85,4 +85,9 @@ bool AppleVZ::is_supported() const { return multipass::applevz::is_supported(); } + +std::vector AppleVZ::bridged_network_interfaces() const +{ + return multipass::applevz::bridged_network_interfaces(); +} } // namespace multipass::applevz diff --git a/src/platform/backends/applevz/applevz_wrapper.h b/src/platform/backends/applevz/applevz_wrapper.h index 2995b530d7..896159102f 100644 --- a/src/platform/backends/applevz/applevz_wrapper.h +++ b/src/platform/backends/applevz/applevz_wrapper.h @@ -21,7 +21,6 @@ #include #include -#include #define MP_APPLEVZ multipass::applevz::AppleVZ::instance() @@ -51,5 +50,8 @@ class AppleVZ : public Singleton virtual bool can_request_stop(const VMHandle& vm_handle) const; virtual bool is_supported() const; + + // Networking + virtual std::vector bridged_network_interfaces() const; }; } // namespace multipass::applevz diff --git a/tests/unit/applevz/CMakeLists.txt b/tests/unit/applevz/CMakeLists.txt index 89f95389a8..d41e0485d9 100644 --- a/tests/unit/applevz/CMakeLists.txt +++ b/tests/unit/applevz/CMakeLists.txt @@ -17,4 +17,5 @@ target_sources(multipass_tests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/test_applevz_utils.cpp ${CMAKE_CURRENT_LIST_DIR}/test_applevz_virtual_machine.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_applevz_virtual_machine_factory.cpp ) diff --git a/tests/unit/applevz/mock_applevz_wrapper.h b/tests/unit/applevz/mock_applevz_wrapper.h index fdf5ba5b23..9116b9f506 100644 --- a/tests/unit/applevz/mock_applevz_wrapper.h +++ b/tests/unit/applevz/mock_applevz_wrapper.h @@ -21,6 +21,7 @@ #include "tests/unit/mock_singleton_helpers.h" #include +#include namespace multipass::test { @@ -71,6 +72,10 @@ class MockAppleVZWrapper : public multipass::applevz::AppleVZ (const multipass::applevz::VMHandle& vm_handle), (const, override)); MOCK_METHOD(bool, is_supported, (), (const, override)); + MOCK_METHOD(std::vector, + bridged_network_interfaces, + (), + (const, override)); MP_MOCK_SINGLETON_BOILERPLATE(MockAppleVZWrapper, AppleVZ); }; diff --git a/tests/unit/applevz/test_applevz_virtual_machine_factory.cpp b/tests/unit/applevz/test_applevz_virtual_machine_factory.cpp new file mode 100644 index 0000000000..e2d535ad3a --- /dev/null +++ b/tests/unit/applevz/test_applevz_virtual_machine_factory.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "mock_applevz_utils.h" +#include "mock_applevz_wrapper.h" +#include "tests/unit/common.h" +#include "tests/unit/mock_cloud_init_file_ops.h" +#include "tests/unit/mock_logger.h" +#include "tests/unit/stub_availability_zone_manager.h" +#include "tests/unit/stub_ssh_key_provider.h" +#include "tests/unit/stub_status_monitor.h" +#include "tests/unit/temp_dir.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace mp = multipass; +namespace mpt = multipass::test; + +using namespace testing; + +namespace +{ +struct AppleVZVirtualMachineFactory_UnitTests : public ::testing::Test +{ + mpt::TempDir dummy_data_dir; + mpt::StubAvailabilityZoneManager stub_az_manager{}; + mpt::StubSSHKeyProvider stub_key_provider{}; + mpt::StubVMStatusMonitor stub_monitor{}; + + mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); + + mpt::MockAppleVZWrapper::GuardedMock mock_applevz_wrapper_injection{ + mpt::MockAppleVZWrapper::inject()}; + mpt::MockAppleVZWrapper& mock_applevz = *mock_applevz_wrapper_injection.first; + + mpt::MockAppleVZUtils::GuardedMock mock_applevz_utils_injection{ + mpt::MockAppleVZUtils::inject()}; + mpt::MockAppleVZUtils& mock_applevz_utils = *mock_applevz_utils_injection.first; + + inline static auto mock_handle_raw = + reinterpret_cast(0xbadf00d); + mp::applevz::VMHandle mock_handle{mock_handle_raw, [](mp::applevz::VirtualMachineHandle*) {}}; + + auto construct_factory() + { + return std::make_shared(dummy_data_dir.path(), + stub_az_manager); + } +}; +} // namespace + +// --------------------------------------------------------- + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, prepareSourceImage) +{ + const std::filesystem::path source_path = "/original/image.img"; + const std::filesystem::path converted_path = "/converted/image.raw"; + + mp::VMImage source_image; + source_image.image_path = source_path; + + EXPECT_CALL(mock_applevz_utils, convert_to_supported_format(source_path, _)) + .WillOnce(Return(converted_path)); + + auto uut = construct_factory(); + const auto result = uut->prepare_source_image(source_image); + + EXPECT_EQ(result.image_path, converted_path); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, prepareInstanceImage) +{ + mp::VMImage instance_image; + instance_image.image_path = "/instance/image.raw"; + + mp::VirtualMachineDescription desc; + desc.disk_space = mp::MemorySize::from_bytes(10 * 1024 * 1024); + + EXPECT_CALL(mock_applevz_utils, resize_image(desc.disk_space, instance_image.image_path)); + + auto uut = construct_factory(); + EXPECT_NO_THROW(uut->prepare_instance_image(instance_image, desc)); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, prepareInstanceImageResizeFails) +{ + mp::VMImage instance_image; + instance_image.image_path = "/instance/image.raw"; + + mp::VirtualMachineDescription desc; + desc.disk_space = mp::MemorySize::from_bytes(10 * 1024 * 1024); + + EXPECT_CALL(mock_applevz_utils, resize_image(desc.disk_space, instance_image.image_path)) + .WillOnce(Throw(std::runtime_error{"resize failed"})); + + auto uut = construct_factory(); + EXPECT_THROW(uut->prepare_instance_image(instance_image, desc), std::runtime_error); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, hypervisorHealthCheckSupported) +{ + EXPECT_CALL(mock_applevz, is_supported()).WillOnce(Return(true)); + + auto uut = construct_factory(); + EXPECT_NO_THROW(uut->hypervisor_health_check()); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, hypervisorHealthCheckNotSupported) +{ + EXPECT_CALL(mock_applevz, is_supported()).WillOnce(Return(false)); + + auto uut = construct_factory(); + EXPECT_THROW(uut->hypervisor_health_check(), std::runtime_error); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, removeResourcesForDeletesInstanceDirectory) +{ + const std::string vm_name = "test-vm"; + auto uut = construct_factory(); + + QDir instance_dir{uut->get_instance_directory(vm_name)}; + ASSERT_TRUE(instance_dir.mkpath(".")); + ASSERT_TRUE(instance_dir.exists()); + + EXPECT_NO_THROW(uut->remove_resources_for(vm_name)); + + EXPECT_FALSE(instance_dir.exists()); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, removeResourcesForNonExistentVmDoesNotThrow) +{ + auto uut = construct_factory(); + EXPECT_NO_THROW(uut->remove_resources_for("does-not-exist")); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, networksReturnsBridgedInterfaces) +{ + const std::vector expected_interfaces = { + {.id = "en0", .type = "Ethernet", .description = "Built-in Ethernet"}, + {.id = "en1", .type = "Wi-Fi", .description = "Wi-Fi"}}; + + EXPECT_CALL(mock_applevz, bridged_network_interfaces()).WillOnce(Return(expected_interfaces)); + + auto uut = construct_factory(); + EXPECT_EQ(uut->networks(), expected_interfaces); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, createVirtualMachine) +{ + mp::VirtualMachineDescription desc; + desc.vm_name = "test-vm"; + + EXPECT_CALL(mock_applevz_utils, convert_to_supported_format(_, _)) + .WillRepeatedly(ReturnArg<0>()); + + EXPECT_CALL(mock_applevz, create_vm(_, _)) + .WillOnce(DoAll(SetArgReferee<1>(mock_handle), Return(mp::applevz::CFError{}))); + + EXPECT_CALL(mock_applevz, get_state(_)) + .WillRepeatedly(Return(mp::applevz::AppleVMState::stopped)); + + auto uut = construct_factory(); + auto vm = uut->create_virtual_machine(desc, stub_key_provider, stub_monitor); + + EXPECT_NE(vm, nullptr); +} + +TEST_F(AppleVZVirtualMachineFactory_UnitTests, cloneCopiesRelevantFiles) +{ + auto uut = construct_factory(); + + const mpt::MockCloudInitFileOps::GuardedMock mock_cloud_init_file_ops_injection = + mpt::MockCloudInitFileOps::inject(); + EXPECT_CALL(*mock_cloud_init_file_ops_injection.first, update_identifiers(_, _, _, _)).Times(1); + + namespace fs = std::filesystem; + const fs::path instances_dir{dummy_data_dir.filePath("applevz/vault/instances").toStdString()}; + constexpr auto* src_vm_name = "dummy_src_name"; + constexpr auto* dest_vm_name = "dummy_dest_name"; + + const fs::path src_vm_dir = instances_dir / src_vm_name; + const fs::path dest_vm_dir = instances_dir / dest_vm_name; + + const std::list source_files{"dummy.iso", "dummy.raw"}; + const std::unordered_set expected_files{"dummy.iso", "dummy.raw"}; + + fs::create_directories(src_vm_dir); + for (const auto& file : source_files) + { + std::ofstream(src_vm_dir / file); + } + + EXPECT_CALL(mock_applevz, create_vm(_, _)) + .WillOnce(DoAll(SetArgReferee<1>(mock_handle), Return(mp::applevz::CFError{}))); + EXPECT_CALL(mock_applevz, get_state(_)) + .WillRepeatedly(Return(mp::applevz::AppleVMState::stopped)); + EXPECT_CALL(mock_applevz_utils, convert_to_supported_format(_, _)) + .WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(mock_applevz_utils, resize_image(_, _)).WillRepeatedly(Return()); + + EXPECT_TRUE( + uut->clone_bare_vm({}, {}, src_vm_name, dest_vm_name, {}, stub_key_provider, stub_monitor)); + + std::unordered_set actual_files; + for (const auto& file : fs::directory_iterator(dest_vm_dir)) + { + if (fs::is_regular_file(file.status())) + { + actual_files.insert(file.path().filename().string()); + } + } + + EXPECT_EQ(actual_files, expected_files); +}