From 647ccefd86c4c5e126948e696b5d9d4592ed48bc Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 14:42:50 +0300 Subject: [PATCH 01/17] [hyperv-hcn] Implement query_endpoint function --- .../hyperv_api/hcn/hyperv_hcn_api.cpp | 7 + .../backends/hyperv_api/hcn/hyperv_hcn_api.h | 4 + .../hyperv_api/hcn/hyperv_hcn_endpoint_info.h | 35 ++++ .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 73 +++++++ .../hyperv_api/hcn/hyperv_hcn_wrapper.h | 3 + tests/unit/hyperv_api/mock_hyperv_hcn_api.h | 4 + .../unit/hyperv_api/mock_hyperv_hcn_wrapper.h | 5 + .../hyperv_api/test_ut_hyperv_hcn_api.cpp | 181 ++++++++++++++++++ 8 files changed, 312 insertions(+) create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp index 7a06434c94..d1e942935d 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp @@ -68,6 +68,13 @@ HRESULT HCNAPI::HcnEnumerateEndpoints(PCWSTR Query, PWSTR* Endpoints, PWSTR* Err { return ::HcnEnumerateEndpoints(Query, Endpoints, ErrorRecord); } +HRESULT HCNAPI::HcnQueryEndpointProperties(HCN_ENDPOINT Endpoint, + PCWSTR Query, + PWSTR* Properties, + PWSTR* ErrorRecord) const +{ + return ::HcnQueryEndpointProperties(Endpoint, Query, Properties, ErrorRecord); +} HRESULT HCNAPI::HcnEnumerateNetworks(PCWSTR Query, PWSTR* Networks, PWSTR* ErrorRecord) const { return ::HcnEnumerateNetworks(Query, Networks, ErrorRecord); diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h index 5712b85ca5..5f1ac3d569 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h @@ -52,6 +52,10 @@ struct HCNAPI : public Singleton [[nodiscard]] virtual HRESULT HcnEnumerateEndpoints(PCWSTR Query, PWSTR* Endpoints, PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnQueryEndpointProperties(HCN_ENDPOINT Endpoint, + PCWSTR Query, + PWSTR* Properties, + PWSTR* ErrorRecord) const; [[nodiscard]] virtual HRESULT HcnEnumerateNetworks(PCWSTR Query, PWSTR* Networks, PWSTR* ErrorRecord) const; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h new file mode 100644 index 0000000000..39f26a4d18 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h @@ -0,0 +1,35 @@ +/* + * 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::hyperv::hcn +{ +struct HcnIpConfiguration +{ + std::string ip_address; +}; + +struct HcnEndpointInfo +{ + std::string guid; + std::vector ip_configurations; +}; +} // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index ab8670cdca..e56c622ee3 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -186,6 +186,20 @@ std::pair open_network(const std::string& net return std::make_pair(result, std::move(network)); } +std::pair open_endpoint(const std::string& endpoint_guid) +{ + mpl::trace(log_category, "open_endpoint(...) > endpoint_guid: {}", endpoint_guid); + + UniqueHcnEndpoint endpoint{}; + const auto result = perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnOpenEndpoint(guid_from_string(endpoint_guid), + out_ptr(endpoint), + rmsgbuf); + }); + + return std::make_pair(result, std::move(endpoint)); +} + } // namespace // --------------------------------------------------------- @@ -263,6 +277,65 @@ OperationResult HCNWrapper::delete_endpoint(const std::string& endpoint_guid) co // --------------------------------------------------------- +OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, + HcnEndpointInfo& out_info) const +{ + mpl::trace(log_category, + "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", + endpoint_guid); + + auto [open_result, endpoint] = open_endpoint(endpoint_guid); + if (!open_result) + return open_result; + + UniqueCotaskmemString properties{}, result_msgbuf{}; + const auto result = ResultCode{API().HcnQueryEndpointProperties(endpoint.get(), + L"{}", + out_ptr(properties), + out_ptr(result_msgbuf))}; + if (!result.success()) + return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; + + if (!properties) + return {E_UNEXPECTED, L"HCN returned no endpoint properties"}; + + const auto properties_as_str = wchar_to_utf8(properties.get()); + mpl::trace(log_category, "query_endpoint result: {}", properties_as_str); + + std::error_code ec; + const auto parsed = boost::json::parse(properties_as_str, ec); + if (ec || !parsed.is_object()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + HcnEndpointInfo endpoint_info{.guid = endpoint_guid}; + const auto* configurations = parsed.as_object().if_contains("IpConfigurations"); + if (configurations) + { + if (!configurations->is_array()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + for (const auto& configuration : configurations->as_array()) + { + if (!configuration.is_object()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + const auto* address = configuration.as_object().if_contains("IpAddress"); + if (!address) + continue; + if (!address->is_string()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + endpoint_info.ip_configurations.push_back( + {.ip_address = std::string{address->as_string()}}); + } + } + + out_info = std::move(endpoint_info); + return {result, L""}; +} + +// --------------------------------------------------------- + OperationResult HCNWrapper::enumerate_attached_endpoints( const std::string& vm_guid, std::vector& endpoint_guids) const diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h index 11a634f934..b20bf4e927 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -44,6 +45,8 @@ struct HCNWrapper : public Singleton [[nodiscard]] virtual OperationResult create_endpoint( const CreateEndpointParameters& params) const; [[nodiscard]] virtual OperationResult delete_endpoint(const std::string& endpoint_guid) const; + [[nodiscard]] virtual OperationResult query_endpoint(const std::string& endpoint_guid, + HcnEndpointInfo& out_info) const; [[nodiscard]] virtual OperationResult enumerate_attached_endpoints( const std::string& vm_guid, std::vector& endpoint_guids) const; diff --git a/tests/unit/hyperv_api/mock_hyperv_hcn_api.h b/tests/unit/hyperv_api/mock_hyperv_hcn_api.h index ad064de7e0..d1272fb205 100644 --- a/tests/unit/hyperv_api/mock_hyperv_hcn_api.h +++ b/tests/unit/hyperv_api/mock_hyperv_hcn_api.h @@ -51,6 +51,10 @@ class MockHCNAPI : public hyperv::hcn::HCNAPI HcnOpenEndpoint, (REFGUID Id, PHCN_ENDPOINT Endpoint, PWSTR* ErrorRecord), (const override)); + MOCK_METHOD(HRESULT, + HcnQueryEndpointProperties, + (HCN_ENDPOINT Endpoint, PCWSTR Query, PWSTR* Properties, PWSTR* ErrorRecord), + (const override)); MOCK_METHOD(HRESULT, HcnDeleteEndpoint, (REFGUID Id, PWSTR* ErrorRecord), (const override)); MOCK_METHOD(HRESULT, HcnCloseEndpoint, (HCN_ENDPOINT Endpoint), (const override)); MOCK_METHOD(HRESULT, diff --git a/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h b/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h index 81a16b55cf..392d3f6789 100644 --- a/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h +++ b/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h @@ -51,6 +51,11 @@ struct MockHCNWrapper : public hyperv::hcn::HCNWrapper (const std::string& endpoint_guid), (const, override)); + MOCK_METHOD(hyperv::OperationResult, + query_endpoint, + (const std::string& endpoint_guid, hyperv::hcn::HcnEndpointInfo& out_info), + (const, override)); + MOCK_METHOD(hyperv::OperationResult, enumerate_attached_endpoints, (const std::string& vm_guid, std::vector& endpoint_guids), diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index 6e82864606..ab5cdccd57 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -764,4 +765,184 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_failure) } } +// --------------------------------------------------------- + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) +{ + static wchar_t endpoint_properties[] = + LR"({"IpConfigurations":[{"IpAddress":"172.20.1.2","PrefixLength":20},{"IpAddress":"fe80::1","PrefixLength":64}]})"; + + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID id, PHCN_ENDPOINT endpoint, PWSTR* error_record) { + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(id)); + ASSERT_EQ(nullptr, *endpoint); + ASSERT_EQ(nullptr, *error_record); + *endpoint = mock_endpoint_object; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&](HCN_ENDPOINT endpoint, PCWSTR query, PWSTR* properties, PWSTR* error_record) { + ASSERT_EQ(mock_endpoint_object, endpoint); + ASSERT_STREQ(L"{}", query); + ASSERT_EQ(nullptr, *properties); + ASSERT_EQ(nullptr, *error_record); + *properties = endpoint_properties; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + ASSERT_TRUE(result); + EXPECT_EQ(endpoint_info.guid, "af3fb745-2f23-463c-8ded-443f876d9e81"); + ASSERT_EQ(endpoint_info.ip_configurations.size(), 2); + EXPECT_EQ(endpoint_info.ip_configurations[0].ip_address, "172.20.1.2"); + EXPECT_EQ(endpoint_info.ip_configurations[1].ip_address, "fe80::1"); +} + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_open_failure) +{ + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID, PHCN_ENDPOINT, PWSTR* error_record) { *error_record = mock_error_msg; }, + Return(E_POINTER))); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(mock_error_msg)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(result.code), E_POINTER); + EXPECT_STREQ(result.status_msg.c_str(), mock_error_msg); +} + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_query_failure) +{ + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&](HCN_ENDPOINT, PCWSTR, PWSTR*, PWSTR* error_record) { + *error_record = mock_error_msg; + }, + Return(E_POINTER))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(mock_error_msg)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(result.code), E_POINTER); + EXPECT_STREQ(result.status_msg.c_str(), mock_error_msg); +} + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_unassigned_ip) +{ + static wchar_t endpoint_properties[] = LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81"})"; + + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { + *properties = endpoint_properties; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + EXPECT_TRUE(result); + EXPECT_TRUE(endpoint_info.ip_configurations.empty()); +} + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) +{ + static wchar_t endpoint_properties[] = LR"({"IpConfigurations":"invalid"})"; + + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { + *properties = endpoint_properties; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(result.code), E_UNEXPECTED); + EXPECT_TRUE(endpoint_info.ip_configurations.empty()); +} + } // namespace multipass::test From 44976aa326f932a4498f8c81c78ac6fb672615b6 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 14:46:10 +0300 Subject: [PATCH 02/17] [hcs-vm] Use HCN ep query for management_ipv4 The hostname resolution approach is unreliable because the hosts.ics file tends to get corrupted, leading to connectivity issues. --- .../hyperv_api/hcs_virtual_machine.cpp | 126 ++++-------------- .../test_ut_hyperv_hcs_virtual_machine.cpp | 50 +++++++ 2 files changed, 74 insertions(+), 102 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index f7b8e1503f..0e43256fc7 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -26,7 +26,6 @@ #include #include -#include #include #include @@ -40,8 +39,6 @@ #include -#include - namespace { @@ -69,97 +66,6 @@ inline auto replace_colon_with_dash(const std::string& addr) return result; } -/** - * Perform a DNS resolve of @p hostname to obtain IPv4/IPv6 - * address(es) associated with it. - * - * @param [in] hostname Hostname to resolve - * @return Vector of IPv4/IPv6 addresses - */ -auto resolve_ip_addresses(const std::string& hostname) -{ - const static mp::wsa_init_wrapper wsa_context{}; - - std::vector ipv4{}, ipv6{}; - mpl::trace("resolve-ip-addr", - "resolve_ip_addresses() -> resolve being called for hostname `{}`", - hostname); - - // Wrap the raw addrinfo pointer so it's always destroyed properly. - const auto& [result, addr_info] = [&]() { - struct addrinfo* result = {nullptr}; - // clang-format off - // (xmkg): different behavior between clang-format versions. - struct addrinfo hints - { - - }; - // clang-format on - const auto r = getaddrinfo(hostname.c_str(), nullptr, nullptr, &result); - return std::make_pair( - r, - std::unique_ptr{result, freeaddrinfo}); - }(); - - if (result == 0) - { - assert(addr_info.get()); - for (auto ptr = addr_info.get(); ptr != nullptr; ptr = ptr->ai_next) - { - switch (ptr->ai_family) - { - case AF_INET: - { - constexpr auto sockaddr_in_size = sizeof(std::remove_pointer_t); - if (ptr->ai_addrlen >= sockaddr_in_size) - { - const auto sockaddr_ipv4 = reinterpret_cast(ptr->ai_addr); - char addr[INET_ADDRSTRLEN] = {}; - inet_ntop(AF_INET, &(sockaddr_ipv4->sin_addr), addr, sizeof(addr)); - ipv4.push_back(addr); - break; - } - - mpl::error("resolve-ip-addr", - "resolve_ip_addresses() -> anomaly: received {} bytes of IPv4 address " - "data while expecting {}!", - ptr->ai_addrlen, - sockaddr_in_size); - } - break; - case AF_INET6: - { - constexpr auto sockaddr_in6_size = sizeof(std::remove_pointer_t); - if (ptr->ai_addrlen >= sockaddr_in6_size) - { - const auto sockaddr_ipv6 = reinterpret_cast(ptr->ai_addr); - char addr[INET6_ADDRSTRLEN] = {}; - inet_ntop(AF_INET6, &(sockaddr_ipv6->sin6_addr), addr, sizeof(addr)); - ipv6.push_back(addr); - break; - } - mpl::error("resolve-ip-addr", - "resolve_ip_addresses() -> anomaly: received {} bytes of IPv6 address " - "data while expecting {}!", - ptr->ai_addrlen, - sockaddr_in6_size); - } - break; - default: - continue; - } - } - } - - mpl::trace("resolve-ip-addr", - "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", - hostname, - fmt::join(ipv4, ","), - fmt::join(ipv6, ",")); - - return std::make_pair(ipv4, ipv6); -} - void try_create_endpoints( const std::string& vm_name, const std::vector& create_endpoint_params) @@ -609,19 +515,35 @@ std::string HCSVirtualMachine::ssh_username() std::optional HCSVirtualMachine::management_ipv4() { - const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname().c_str()); - if (ipv4.empty()) + const auto endpoint_guid = mac2uuid(description.default_mac_address); + hcn::HcnEndpointInfo endpoint_info; + if (const auto query_result = HCN().query_endpoint(endpoint_guid, endpoint_info); !query_result) { - mpl::error(get_name(), "management_ipv4() > failed to resolve `{}`", ssh_hostname()); + mpl::error(get_name(), + "management_ipv4() > failed to query endpoint `{}`: {}", + endpoint_guid, + query_result); return std::nullopt; } - const auto result = *ipv4.begin(); - - mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", result); + for (const auto& configuration : endpoint_info.ip_configurations) + { + try + { + IPAddress address{configuration.ip_address}; + mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); + return address; + } + catch (const std::invalid_argument&) + { + // HCN also reports IPv6 configurations, which IPAddress does not represent. + } + } - // Prefer the first one - return std::make_optional(result); + mpl::debug(get_name(), + "management_ipv4() > endpoint `{}` has no IPv4 configuration", + endpoint_guid); + return std::nullopt; } void HCSVirtualMachine::handle_state_update() diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 6bb1244fc9..50ad6ccf7f 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -529,6 +529,56 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) // --------------------------------------------------------- +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_primary_endpoint) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = { + {.ip_address = "fe80::1"}, {.ip_address = "10.123.45.67"}}; + }, + Return(hcs_op_result_t{0, L""}))); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); +} + +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_when_query_fails) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->management_ipv4(), std::nullopt); +} + +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_ipv4_configuration) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = {{.ip_address = "fe80::1"}}; + }, + Return(hcs_op_result_t{0, L""}))); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->management_ipv4(), std::nullopt); +} + +// --------------------------------------------------------- + TEST_F(HyperVHCSVirtualMachine_UnitTests, update_state) { default_open_success(); From e66658bfa0da488fa8ef1ea585dc8f8de607916d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 15:04:57 +0300 Subject: [PATCH 03/17] [hyperv-hcs] Use management_ip for ssh_hostname --- .../hyperv_api/hcs_virtual_machine.cpp | 2 +- .../test_ut_hyperv_hcs_virtual_machine.cpp | 70 ++++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 0e43256fc7..fa8374eb2a 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -506,7 +506,7 @@ int HCSVirtualMachine::ssh_port() } std::string HCSVirtualMachine::ssh_hostname() { - return fmt::format("{}.mshome.net", get_name()); + return require_management_ipv4().as_string(); } std::string HCSVirtualMachine::ssh_username() { diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 50ad6ccf7f..be0b5ee370 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -522,9 +523,30 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) { default_open_success(); - std::shared_ptr uut{nullptr}; - ASSERT_NO_THROW(uut = construct_vm()); - EXPECT_EQ(uut->ssh_hostname(), uut->get_name() + ".mshome.net"); + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; + }, + Return(hcs_op_result_t{0, L""}))); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->ssh_hostname(), "10.123.45.67"); +} + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname_throws_when_ip_is_unavailable) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); + + auto uut = construct_vm(); + + EXPECT_THROW((void)uut->ssh_hostname(), mp::IPUnavailableException); } // --------------------------------------------------------- @@ -547,6 +569,48 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_primary_endpoi EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); } +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_each_time) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; + }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = {{.ip_address = "10.123.45.68"}}; + }, + Return(hcs_op_result_t{0, L""}))); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); + EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.68"}); +} + +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_retries_unsuccessful_query) +{ + default_open_success(); + + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})) + .WillOnce(DoAll( + [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; + }, + Return(hcs_op_result_t{0, L""}))); + + auto uut = construct_vm(); + + EXPECT_EQ(uut->management_ipv4(), std::nullopt); + EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); +} + TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_when_query_fails) { default_open_success(); From 996c29ca3f90e490d7d5c4b9dd80ba6f17b33ad1 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 15:13:06 +0300 Subject: [PATCH 04/17] [hyperv-hcs] Add it/bb tests for IP --- .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 13 ++++++- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 33 +++++++++++++++++ .../hyperv_api/test_it_hyperv_hcn_api.cpp | 9 +++++ .../hyperv_api/test_ut_hyperv_hcn_api.cpp | 37 +++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index e56c622ee3..11eda6bc6f 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -284,7 +284,7 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", endpoint_guid); - auto [open_result, endpoint] = open_endpoint(endpoint_guid); + const auto & [open_result, endpoint] = open_endpoint(endpoint_guid); if (!open_result) return open_result; @@ -308,7 +308,8 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; HcnEndpointInfo endpoint_info{.guid = endpoint_guid}; - const auto* configurations = parsed.as_object().if_contains("IpConfigurations"); + const auto& endpoint_object = parsed.as_object(); + const auto* configurations = endpoint_object.if_contains("IpConfigurations"); if (configurations) { if (!configurations->is_array()) @@ -329,6 +330,14 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, {.ip_address = std::string{address->as_string()}}); } } + else if (const auto* address = endpoint_object.if_contains("IPAddress")) + { + if (!address->is_string()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + endpoint_info.ip_configurations.push_back( + {.ip_address = std::string{address->as_string()}}); + } out_info = std::move(endpoint_info); return {result, L""}; diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index b85ac632c7..b1195750c0 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -22,16 +22,23 @@ #include #include +#include #include #include #include +#include + +#include +#include + namespace multipass::test { using namespace hyperv::hcs; using hyperv::hcn::HCN; using hyperv::virtdisk::VirtDisk; +using namespace std::chrono_literals; // Component level big bang integration tests for Hyper-V HCN/HCS + virtdisk API's. // These tests ensure that the API's working together as expected. @@ -120,6 +127,32 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) ASSERT_TRUE(status.success()); } + // HCN assigns endpoint addresses asynchronously, so allow a bounded wait. + std::optional endpoint_ipv4; + for (auto attempts = 0; attempts < 20 && !endpoint_ipv4; ++attempts) + { + hyperv::hcn::HcnEndpointInfo endpoint_info; + const auto result = HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); + ASSERT_TRUE(result); + + for (const auto& configuration : endpoint_info.ip_configurations) + { + try + { + endpoint_ipv4.emplace(configuration.ip_address); + break; + } + catch (const std::invalid_argument&) + { + // Ignore IPv6 configurations; IPAddress only represents IPv4. + } + } + + if (!endpoint_ipv4) + std::this_thread::sleep_for(250ms); + } + ASSERT_TRUE(endpoint_ipv4); + (void)HCS().terminate_compute_system(handle); handle.reset(); (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index e6b45394c2..579d7a9d9a 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace multipass::test @@ -153,6 +154,14 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) ASSERT_TRUE(error_msg.empty()); } + { + HcnEndpointInfo endpoint_info; + const auto result = HCN().query_endpoint(endpoint_params.endpoint_guid, endpoint_info); + ASSERT_TRUE(result); + EXPECT_EQ(endpoint_info.guid, endpoint_params.endpoint_guid); + EXPECT_FALSE(endpoint_info.ip_configurations.empty()); + } + { const auto& [status, error_msg] = HCN().delete_endpoint(endpoint_params.endpoint_guid); ASSERT_TRUE(status.success()); diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index ab5cdccd57..d3499253ae 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -909,6 +909,43 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_unassigned_ip) EXPECT_TRUE(endpoint_info.ip_configurations.empty()); } +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_flattened_ip_configuration) +{ + static wchar_t endpoint_properties[] = + LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81","IPAddress":"172.20.1.2","PrefixLength":20})"; + + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { + *properties = endpoint_properties; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = + HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + ASSERT_TRUE(result); + ASSERT_EQ(endpoint_info.ip_configurations.size(), 1); + EXPECT_EQ(endpoint_info.ip_configurations[0].ip_address, "172.20.1.2"); +} + TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) { static wchar_t endpoint_properties[] = LR"({"IpConfigurations":"invalid"})"; From a42254d4cc34055555996463781c1de78e021adb Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 15:26:25 +0300 Subject: [PATCH 05/17] [hyperv-hcn] Simplify query_endpoint impl --- .../hyperv_api/hcn/hyperv_hcn_endpoint_info.h | 8 +- .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 72 ++++---- .../hyperv_api/hcs_virtual_machine.cpp | 4 +- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 17 +- .../hyperv_api/test_it_hyperv_hcn_api.cpp | 11 +- .../hyperv_api/test_ut_hyperv_hcn_api.cpp | 160 ++++++------------ .../test_ut_hyperv_hcs_virtual_machine.cpp | 62 +++---- 7 files changed, 145 insertions(+), 189 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h index 39f26a4d18..16bdfedadc 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h @@ -22,14 +22,8 @@ namespace multipass::hyperv::hcn { -struct HcnIpConfiguration -{ - std::string ip_address; -}; - struct HcnEndpointInfo { - std::string guid; - std::vector ip_configurations; + std::vector ip_addresses; }; } // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index 11eda6bc6f..e20e5c4cb6 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include @@ -200,6 +201,39 @@ std::pair open_endpoint(const std::string& e return std::make_pair(result, std::move(endpoint)); } +std::optional> endpoint_ip_addresses(const boost::json::object& endpoint) +{ + std::vector addresses; + const auto append_address = [&addresses](const boost::json::value& address) { + if (!address.is_string()) + return false; + + addresses.emplace_back(address.as_string()); + return true; + }; + + if (const auto* configurations = endpoint.if_contains("IpConfigurations")) + { + if (!configurations->is_array()) + return std::nullopt; + + for (const auto& configuration : configurations->as_array()) + { + if (!configuration.is_object()) + return std::nullopt; + + if (const auto* address = configuration.as_object().if_contains("IpAddress"); + address && !append_address(*address)) + return std::nullopt; + } + } + + if (const auto* address = endpoint.if_contains("IPAddress"); address && !append_address(*address)) + return std::nullopt; + + return addresses; +} + } // namespace // --------------------------------------------------------- @@ -284,7 +318,7 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", endpoint_guid); - const auto & [open_result, endpoint] = open_endpoint(endpoint_guid); + const auto& [open_result, endpoint] = open_endpoint(endpoint_guid); if (!open_result) return open_result; @@ -307,39 +341,11 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, if (ec || !parsed.is_object()) return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - HcnEndpointInfo endpoint_info{.guid = endpoint_guid}; - const auto& endpoint_object = parsed.as_object(); - const auto* configurations = endpoint_object.if_contains("IpConfigurations"); - if (configurations) - { - if (!configurations->is_array()) - return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - - for (const auto& configuration : configurations->as_array()) - { - if (!configuration.is_object()) - return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - - const auto* address = configuration.as_object().if_contains("IpAddress"); - if (!address) - continue; - if (!address->is_string()) - return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - - endpoint_info.ip_configurations.push_back( - {.ip_address = std::string{address->as_string()}}); - } - } - else if (const auto* address = endpoint_object.if_contains("IPAddress")) - { - if (!address->is_string()) - return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - - endpoint_info.ip_configurations.push_back( - {.ip_address = std::string{address->as_string()}}); - } + auto addresses = endpoint_ip_addresses(parsed.as_object()); + if (!addresses) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - out_info = std::move(endpoint_info); + out_info.ip_addresses = std::move(*addresses); return {result, L""}; } diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index fa8374eb2a..335959b597 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -526,11 +526,11 @@ std::optional HCSVirtualMachine::management_ipv4() return std::nullopt; } - for (const auto& configuration : endpoint_info.ip_configurations) + for (const auto& ip_address : endpoint_info.ip_addresses) { try { - IPAddress address{configuration.ip_address}; + IPAddress address{ip_address}; mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); return address; } diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index b1195750c0..292264360b 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -29,6 +29,8 @@ #include +#include + #include #include @@ -67,6 +69,16 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) return endpoint_parameters; }(); + auto cleanup = sg::make_scope_guard([&]() noexcept { + if (handle) + { + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); + }); + const auto temp_path = make_tempfile_path(".vhdx"); const hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{ @@ -135,11 +147,11 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) const auto result = HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); ASSERT_TRUE(result); - for (const auto& configuration : endpoint_info.ip_configurations) + for (const auto& ip_address : endpoint_info.ip_addresses) { try { - endpoint_ipv4.emplace(configuration.ip_address); + endpoint_ipv4.emplace(ip_address); break; } catch (const std::invalid_argument&) @@ -157,6 +169,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) handle.reset(); (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); (void)HCN().delete_network(network_parameters.guid); + cleanup.dismiss(); } TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_boot) diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index 579d7a9d9a..b78d859b9c 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -23,6 +23,8 @@ #include #include +#include + namespace multipass::test { @@ -139,6 +141,11 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) endpoint_params.network_guid = network_params.guid; endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; + auto cleanup = sg::make_scope_guard([&]() noexcept { + (void)HCN().delete_endpoint(endpoint_params.endpoint_guid); + (void)HCN().delete_network(network_params.guid); + }); + (void)HCN().delete_network(network_params.guid); { @@ -158,8 +165,7 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint(endpoint_params.endpoint_guid, endpoint_info); ASSERT_TRUE(result); - EXPECT_EQ(endpoint_info.guid, endpoint_params.endpoint_guid); - EXPECT_FALSE(endpoint_info.ip_configurations.empty()); + EXPECT_FALSE(endpoint_info.ip_addresses.empty()); } { @@ -173,6 +179,7 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } + cleanup.dismiss(); } TEST_F(HyperVHCNAPI_IntegrationTests, create_endpoint_explicit_mac) diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index d3499253ae..db89f06ea6 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -63,6 +63,45 @@ struct HyperVHCNAPI_UnitTests : public ::testing::Test // Generic error message for all tests, intended to be used for API calls returning // an "error_record". inline static wchar_t mock_error_msg[16] = L"It's a failure."; + + void expect_endpoint_query(wchar_t* endpoint_properties) + { + EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) + .WillOnce(DoAll( + [&](REFGUID id, PHCN_ENDPOINT endpoint, PWSTR* error_record) { + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(id)); + ASSERT_EQ(nullptr, *endpoint); + ASSERT_EQ(nullptr, *error_record); + *endpoint = mock_endpoint_object; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) + .WillOnce(DoAll( + [&, endpoint_properties](HCN_ENDPOINT endpoint, + PCWSTR query, + PWSTR* properties, + PWSTR* error_record) { + ASSERT_EQ(mock_endpoint_object, endpoint); + ASSERT_STREQ(L"{}", query); + ASSERT_EQ(nullptr, *properties); + ASSERT_EQ(nullptr, *error_record); + *properties = endpoint_properties; + }, + Return(NOERROR))); + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); + EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); + + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: " + "af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + } }; // --------------------------------------------------------- @@ -772,47 +811,16 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) static wchar_t endpoint_properties[] = LR"({"IpConfigurations":[{"IpAddress":"172.20.1.2","PrefixLength":20},{"IpAddress":"fe80::1","PrefixLength":64}]})"; - EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) - .WillOnce(DoAll( - [&](REFGUID id, PHCN_ENDPOINT endpoint, PWSTR* error_record) { - ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(id)); - ASSERT_EQ(nullptr, *endpoint); - ASSERT_EQ(nullptr, *error_record); - *endpoint = mock_endpoint_object; - }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) - .WillOnce(DoAll( - [&](HCN_ENDPOINT endpoint, PCWSTR query, PWSTR* properties, PWSTR* error_record) { - ASSERT_EQ(mock_endpoint_object, endpoint); - ASSERT_STREQ(L"{}", query); - ASSERT_EQ(nullptr, *properties); - ASSERT_EQ(nullptr, *error_record); - *properties = endpoint_properties; - }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); - EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); - - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, - "perform_hcn_operation(...) > result: true"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); ASSERT_TRUE(result); - EXPECT_EQ(endpoint_info.guid, "af3fb745-2f23-463c-8ded-443f876d9e81"); - ASSERT_EQ(endpoint_info.ip_configurations.size(), 2); - EXPECT_EQ(endpoint_info.ip_configurations[0].ip_address, "172.20.1.2"); - EXPECT_EQ(endpoint_info.ip_configurations[1].ip_address, "fe80::1"); + ASSERT_EQ(endpoint_info.ip_addresses.size(), 2); + EXPECT_EQ(endpoint_info.ip_addresses[0], "172.20.1.2"); + EXPECT_EQ(endpoint_info.ip_addresses[1], "fe80::1"); } TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_open_failure) @@ -878,100 +886,38 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_unassigned_ip) { static wchar_t endpoint_properties[] = LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81"})"; - EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) - .WillOnce(DoAll( - [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) - .WillOnce(DoAll( - [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { - *properties = endpoint_properties; - }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); - EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); - - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, - "perform_hcn_operation(...) > result: true"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); EXPECT_TRUE(result); - EXPECT_TRUE(endpoint_info.ip_configurations.empty()); + EXPECT_TRUE(endpoint_info.ip_addresses.empty()); } -TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_flattened_ip_configuration) +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_merges_flattened_ip_configuration) { static wchar_t endpoint_properties[] = - LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81","IPAddress":"172.20.1.2","PrefixLength":20})"; + LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81","IpConfigurations":[{"IpAddress":"fe80::1","PrefixLength":64}],"IPAddress":"172.20.1.2","PrefixLength":20})"; - EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) - .WillOnce(DoAll( - [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) - .WillOnce(DoAll( - [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { - *properties = endpoint_properties; - }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); - EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); - - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, - "perform_hcn_operation(...) > result: true"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); ASSERT_TRUE(result); - ASSERT_EQ(endpoint_info.ip_configurations.size(), 1); - EXPECT_EQ(endpoint_info.ip_configurations[0].ip_address, "172.20.1.2"); + ASSERT_EQ(endpoint_info.ip_addresses.size(), 2); + EXPECT_EQ(endpoint_info.ip_addresses[0], "fe80::1"); + EXPECT_EQ(endpoint_info.ip_addresses[1], "172.20.1.2"); } TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) { static wchar_t endpoint_properties[] = LR"({"IpConfigurations":"invalid"})"; - EXPECT_CALL(mock_hcn_api, HcnOpenEndpoint) - .WillOnce(DoAll( - [&](REFGUID, PHCN_ENDPOINT endpoint, PWSTR*) { *endpoint = mock_endpoint_object; }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnQueryEndpointProperties) - .WillOnce(DoAll( - [&](HCN_ENDPOINT, PCWSTR, PWSTR* properties, PWSTR*) { - *properties = endpoint_properties; - }, - Return(NOERROR))); - EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); - EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); - - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "HCNWrapper::query_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, - "perform_hcn_operation(...) > result: true"); - logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; const auto result = @@ -979,7 +925,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) EXPECT_FALSE(result); EXPECT_EQ(static_cast(result.code), E_UNEXPECTED); - EXPECT_TRUE(endpoint_info.ip_configurations.empty()); + EXPECT_TRUE(endpoint_info.ip_addresses.empty()); } } // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index be0b5ee370..b11b229064 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -195,6 +195,24 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test Return(hcs_op_result_t{0, L""}))); } + void expect_endpoint_query(std::vector ip_addresses) + { + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce([ip_addresses = std::move(ip_addresses)]( + const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_addresses = ip_addresses; + return hcs_op_result_t{0, L""}; + }); + } + + void expect_endpoint_query_failure() + { + EXPECT_CALL(mock_hcn, + query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); + } + template std::shared_ptr construct_vm(multipass::VMStatusMonitor* monitor = nullptr) { @@ -522,14 +540,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_port) TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) { default_open_success(); - - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce(DoAll( - [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; - }, - Return(hcs_op_result_t{0, L""}))); + expect_endpoint_query({"10.123.45.67"}); auto uut = construct_vm(); @@ -539,10 +550,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname_throws_when_ip_is_unavailable) { default_open_success(); - - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); + expect_endpoint_query_failure(); auto uut = construct_vm(); @@ -554,15 +562,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname_throws_when_ip_is_unav TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_primary_endpoint) { default_open_success(); - - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce(DoAll( - [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = { - {.ip_address = "fe80::1"}, {.ip_address = "10.123.45.67"}}; - }, - Return(hcs_op_result_t{0, L""}))); + expect_endpoint_query({"fe80::1", "10.123.45.67"}); auto uut = construct_vm(); @@ -577,12 +577,12 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_each_time) query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) .WillOnce(DoAll( [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; + endpoint_info.ip_addresses = {"10.123.45.67"}; }, Return(hcs_op_result_t{0, L""}))) .WillOnce(DoAll( [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = {{.ip_address = "10.123.45.68"}}; + endpoint_info.ip_addresses = {"10.123.45.68"}; }, Return(hcs_op_result_t{0, L""}))); @@ -601,7 +601,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_retries_unsuccessful_q .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})) .WillOnce(DoAll( [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = {{.ip_address = "10.123.45.67"}}; + endpoint_info.ip_addresses = {"10.123.45.67"}; }, Return(hcs_op_result_t{0, L""}))); @@ -614,10 +614,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_retries_unsuccessful_q TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_when_query_fails) { default_open_success(); - - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); + expect_endpoint_query_failure(); auto uut = construct_vm(); @@ -627,14 +624,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_when_que TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_ipv4_configuration) { default_open_success(); - - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce(DoAll( - [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_configurations = {{.ip_address = "fe80::1"}}; - }, - Return(hcs_op_result_t{0, L""}))); + expect_endpoint_query({"fe80::1"}); auto uut = construct_vm(); From 016f3c99f6edde9e302acc7cb8160bcdcd934b4c Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 15:30:22 +0300 Subject: [PATCH 06/17] [hyperv-hcn] Format changes --- .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 16 ++++---- .../hyperv_api/hcs_virtual_machine.cpp | 24 +++++------ tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 22 +++++----- .../hyperv_api/test_ut_hyperv_hcn_api.cpp | 41 ++++++++----------- .../test_ut_hyperv_hcs_virtual_machine.cpp | 23 +++++------ 5 files changed, 57 insertions(+), 69 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index e20e5c4cb6..0802c34220 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -193,9 +193,7 @@ std::pair open_endpoint(const std::string& e UniqueHcnEndpoint endpoint{}; const auto result = perform_hcn_operation([&](auto&& rmsgbuf) { - return API().HcnOpenEndpoint(guid_from_string(endpoint_guid), - out_ptr(endpoint), - rmsgbuf); + return API().HcnOpenEndpoint(guid_from_string(endpoint_guid), out_ptr(endpoint), rmsgbuf); }); return std::make_pair(result, std::move(endpoint)); @@ -228,7 +226,8 @@ std::optional> endpoint_ip_addresses(const boost::json: } } - if (const auto* address = endpoint.if_contains("IPAddress"); address && !append_address(*address)) + if (const auto* address = endpoint.if_contains("IPAddress"); + address && !append_address(*address)) return std::nullopt; return addresses; @@ -314,9 +313,7 @@ OperationResult HCNWrapper::delete_endpoint(const std::string& endpoint_guid) co OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, HcnEndpointInfo& out_info) const { - mpl::trace(log_category, - "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", - endpoint_guid); + mpl::trace(log_category, "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", endpoint_guid); const auto& [open_result, endpoint] = open_endpoint(endpoint_guid); if (!open_result) @@ -448,8 +445,9 @@ OperationResult HCNWrapper::enumerate_networks(std::vector& out_net UniqueCotaskmemString enumerate_result{}, result_msgbuf{}; // List all HCN network GUIDs - const auto result = - API().HcnEnumerateNetworks(L"{}", out_ptr(enumerate_result), out_ptr(result_msgbuf)); + const auto result = API().HcnEnumerateNetworks(L"{}", + out_ptr(enumerate_result), + out_ptr(result_msgbuf)); if (enumerate_result) { // json_output would contain the network GUIDs. diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 335959b597..c58e0ce57e 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -262,8 +262,8 @@ bool HCSVirtualMachine::maybe_create_compute_system() { // Always reset the handle and create a new one. hcs_system.reset(); - auto attach_callback_handler = - sg::make_scope_guard([this]() noexcept { set_compute_system_callback_handler(); }); + auto attach_callback_handler = sg::make_scope_guard( + [this]() noexcept { set_compute_system_callback_handler(); }); if (const auto result = HCS().open_compute_system(get_name(), hcs_system)) { @@ -294,12 +294,12 @@ bool HCSVirtualMachine::maybe_create_compute_system() .read_only = true}}, .network_adapters = [&] { - const auto view = - endpoints | - std::views::transform([](const auto& endpoint) -> hcs::HcsNetworkAdapter { - return {.endpoint_guid = endpoint.endpoint_guid, - .mac_address = endpoint.mac_address.value()}; - }); + const auto view = endpoints | + std::views::transform( + [](const auto& endpoint) -> hcs::HcsNetworkAdapter { + return {.endpoint_guid = endpoint.endpoint_guid, + .mac_address = endpoint.mac_address.value()}; + }); return std::vector(std::ranges::begin(view), std::ranges::end(view)); }(), .guest_state = {.guest_state_file_path = get_guest_state_file_path(), @@ -308,8 +308,8 @@ bool HCSVirtualMachine::maybe_create_compute_system() ? std::optional(get_saved_state_file_path()) : std::nullopt}}; - if (const auto create_result = - HCS().create_compute_system(create_compute_system_params, hcs_system); + if (const auto create_result = HCS().create_compute_system(create_compute_system_params, + hcs_system); !create_result) { throw CreateComputeSystemException{"create_compute_system failed with {}", @@ -574,8 +574,8 @@ void HCSVirtualMachine::resize_disk_impl(const MemorySize& new_size) { mpl::debug(get_name(), "resize_disk() -> new_size `{}` MiB", new_size.in_megabytes()); - if (const auto result = - VirtDisk().resize_virtual_disk(description.image.image_path, new_size.in_bytes()); + if (const auto result = VirtDisk().resize_virtual_disk(description.image.image_path, + new_size.in_bytes()); !result) { throw ResizeDiskException{"Disk resize failed, details: {}", result}; diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 292264360b..67de63c152 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -128,8 +128,8 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) // Create test VM { - const auto& [status, status_msg] = - HCS().create_compute_system(create_vm_parameters, handle); + const auto& [status, status_msg] = HCS().create_compute_system(create_vm_parameters, + handle); ASSERT_TRUE(status.success()); } @@ -263,8 +263,8 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo // Create test VM { - const auto& [status, status_msg] = - HCS().create_compute_system(create_vm_parameters, handle); + const auto& [status, status_msg] = HCS().create_compute_system(create_vm_parameters, + handle); ASSERT_TRUE(status.success()); ASSERT_TRUE(status_msg.empty()); } @@ -285,8 +285,8 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo HcsResourcePath::NetworkAdapters(network_adapter.endpoint_guid), HcsRequestType::Add(), network_adapter}; - const auto& [status, status_msg] = - HCS().modify_compute_system(handle, add_network_adapter_req); + const auto& [status, status_msg] = HCS().modify_compute_system(handle, + add_network_adapter_req); ASSERT_TRUE(status.success()); ASSERT_TRUE(status_msg.empty()); } @@ -295,11 +295,11 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo { // Create another EP so we can ensure that we're only listing the EPs belonging to the VM { - const auto& [status, status_msg] = - HCN().create_endpoint(hyperv::hcn::CreateEndpointParameters{ - .network_guid = network_parameters.guid, - .endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029b", - }); + const auto& [status, + status_msg] = HCN().create_endpoint(hyperv::hcn::CreateEndpointParameters{ + .network_guid = network_parameters.guid, + .endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029b", + }); ASSERT_TRUE(status.success()); ASSERT_TRUE(status_msg.empty()); diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index db89f06ea6..a0e683136b 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -91,10 +91,9 @@ struct HyperVHCNAPI_UnitTests : public ::testing::Test EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint(mock_endpoint_object)).WillOnce(Return(NOERROR)); EXPECT_CALL(mock_hcn_api, CoTaskMemFree(endpoint_properties)); - logger_scope.mock_logger->expect_log( - mpl::Level::trace, - "HCNWrapper::query_endpoint(...) > endpoint_guid: " - "af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::query_endpoint(...) > endpoint_guid: " + "af3fb745-2f23-463c-8ded-443f876d9e81"); logger_scope.mock_logger->expect_log( mpl::Level::trace, "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); @@ -501,8 +500,8 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_network_success) } { // Verify the expected outcome. - const auto& [status, error_msg] = - HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); + const auto& [status, + error_msg] = HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } @@ -537,8 +536,8 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_network_failed) } { // Verify the expected outcome. - const auto& [status, error_msg] = - HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); + const auto& [status, + error_msg] = HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); ASSERT_FALSE(status.success()); ASSERT_FALSE(error_msg.empty()); ASSERT_STREQ(error_msg.c_str(), mock_error_msg); @@ -764,8 +763,8 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_success) } { // Verify the expected outcome. - const auto& [status, error_msg] = - HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); + const auto& [status, + error_msg] = HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } @@ -796,8 +795,8 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_failure) } { // Verify the expected outcome. - const auto& [status, error_msg] = - HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); + const auto& [status, + error_msg] = HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); ASSERT_FALSE(status.success()); ASSERT_FALSE(error_msg.empty()); ASSERT_STREQ(error_msg.c_str(), mock_error_msg); @@ -814,8 +813,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); ASSERT_TRUE(result); ASSERT_EQ(endpoint_info.ip_addresses.size(), 2); @@ -841,8 +839,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_open_failure) "perform_hcn_operation(...) > result: false"); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); EXPECT_FALSE(result); EXPECT_EQ(static_cast(result.code), E_POINTER); @@ -874,8 +871,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_query_failure) "perform_hcn_operation(...) > result: true"); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); EXPECT_FALSE(result); EXPECT_EQ(static_cast(result.code), E_POINTER); @@ -889,8 +885,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_accepts_unassigned_ip) expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); EXPECT_TRUE(result); EXPECT_TRUE(endpoint_info.ip_addresses.empty()); @@ -904,8 +899,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_merges_flattened_ip_configuration) expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); ASSERT_TRUE(result); ASSERT_EQ(endpoint_info.ip_addresses.size(), 2); @@ -920,8 +914,7 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) expect_endpoint_query(endpoint_properties); hcn::HcnEndpointInfo endpoint_info; - const auto result = - HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); EXPECT_FALSE(result); EXPECT_EQ(static_cast(result.code), E_UNEXPECTED); diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index b11b229064..349a291371 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -197,19 +197,18 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test void expect_endpoint_query(std::vector ip_addresses) { - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce([ip_addresses = std::move(ip_addresses)]( - const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { - endpoint_info.ip_addresses = ip_addresses; - return hcs_op_result_t{0, L""}; - }); + EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + .WillOnce( + [ip_addresses = std::move(ip_addresses)](const std::string&, + mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.ip_addresses = ip_addresses; + return hcs_op_result_t{0, L""}; + }); } void expect_endpoint_query_failure() { - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})); } @@ -573,8 +572,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_each_time) { default_open_success(); - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) .WillOnce(DoAll( [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { endpoint_info.ip_addresses = {"10.123.45.67"}; @@ -596,8 +594,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_retries_unsuccessful_q { default_open_success(); - EXPECT_CALL(mock_hcn, - query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) + EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) .WillOnce(Return(hcs_op_result_t{E_FAIL, L"Endpoint query failed"})) .WillOnce(DoAll( [](const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { From d7249a2ec11836fbc86577f4c5c743c49d364344 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 18:01:37 +0300 Subject: [PATCH 07/17] [win] Add permanent_ipv4_neighbor helper --- .../backends/shared/windows/CMakeLists.txt | 2 + .../backends/shared/windows/network_utils.cpp | 105 ++++++++++++++++++ .../backends/shared/windows/network_utils.h | 26 +++++ tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 39 ++----- .../hyperv_api/test_it_hyperv_hcn_api.cpp | 5 +- tests/unit/windows/test_platform_win.cpp | 7 ++ 6 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 src/platform/backends/shared/windows/network_utils.cpp create mode 100644 src/platform/backends/shared/windows/network_utils.h diff --git a/src/platform/backends/shared/windows/CMakeLists.txt b/src/platform/backends/shared/windows/CMakeLists.txt index 84ea03bde4..2bd4a2e88a 100644 --- a/src/platform/backends/shared/windows/CMakeLists.txt +++ b/src/platform/backends/shared/windows/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(shared_win STATIC wsa_init_wrapper.cpp windows_version.cpp guid_formatter.cpp + network_utils.cpp windows_feature_status.cpp) include_directories(shared_win @@ -29,6 +30,7 @@ include_directories(shared_win target_link_libraries(shared_win Qt6::Core fmt::fmt-header-only + Iphlpapi logger OpenSSL::Crypto sftp_client diff --git a/src/platform/backends/shared/windows/network_utils.cpp b/src/platform/backends/shared/windows/network_utils.cpp new file mode 100644 index 0000000000..69e6948164 --- /dev/null +++ b/src/platform/backends/shared/windows/network_utils.cpp @@ -0,0 +1,105 @@ +/* + * 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 version 3 as + * published by the Free Software Foundation. + * + * 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 "network_utils.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace multipass +{ +namespace +{ +constexpr auto log_category = "windows-network"; +constexpr std::size_t ethernet_address_length = 6; + +std::string canonical_mac_address(const unsigned char* address) +{ + return fmt::format("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + address[0], + address[1], + address[2], + address[3], + address[4], + address[5]); +} + +std::string ipv4_to_string(const IN_ADDR& address) +{ + return fmt::format("{}.{}.{}.{}", + address.S_un.S_un_b.s_b1, + address.S_un.S_un_b.s_b2, + address.S_un.S_un_b.s_b3, + address.S_un.S_un_b.s_b4); +} +} // namespace + +std::optional permanent_ipv4_neighbor(const std::string& mac_address) +{ + if (!utils::valid_mac_address(mac_address)) + { + logging::error(log_category, "Invalid MAC address `{}`", mac_address); + return std::nullopt; + } + + PMIB_IPNET_TABLE2 raw_table{}; + if (const auto result = GetIpNetTable2(AF_INET, &raw_table); result != NO_ERROR) + { + logging::error(log_category, "GetIpNetTable2 failed with error code {}", result); + return std::nullopt; + } + + const std::unique_ptr table{raw_table, + &FreeMibTable}; + if (!table) + { + logging::error(log_category, "GetIpNetTable2 returned no neighbor table"); + return std::nullopt; + } + + const auto matches = [&mac_address](const MIB_IPNET_ROW2& row) { + return row.Address.si_family == AF_INET && row.State == NlnsPermanent && + row.PhysicalAddressLength == ethernet_address_length && + std::ranges::equal(canonical_mac_address(row.PhysicalAddress), + mac_address, + [](unsigned char lhs, unsigned char rhs) { + return std::tolower(lhs) == std::tolower(rhs); + }); + }; + + const auto* begin = table->Table; + const auto* end = begin + table->NumEntries; + if (const auto row = std::find_if(begin, end, matches); row != end) + return ipv4_to_string(row->Address.Ipv4.sin_addr); + + return std::nullopt; +} + +} // namespace multipass diff --git a/src/platform/backends/shared/windows/network_utils.h b/src/platform/backends/shared/windows/network_utils.h new file mode 100644 index 0000000000..76bbbbc526 --- /dev/null +++ b/src/platform/backends/shared/windows/network_utils.h @@ -0,0 +1,26 @@ +/* + * 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 version 3 as + * published by the Free Software Foundation. + * + * 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 +{ +[[nodiscard]] std::optional permanent_ipv4_neighbor(const std::string& mac_address); +} diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 67de63c152..7bcc47ee80 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -27,20 +27,14 @@ #include #include -#include - #include -#include -#include - namespace multipass::test { using namespace hyperv::hcs; using hyperv::hcn::HCN; using hyperv::virtdisk::VirtDisk; -using namespace std::chrono_literals; // Component level big bang integration tests for Hyper-V HCN/HCS + virtdisk API's. // These tests ensure that the API's working together as expected. @@ -48,7 +42,7 @@ struct HyperV_ComponentIntegrationTests : public ::testing::Test { }; -TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) +TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network) { hyperv::hcs::HcsSystemHandle handle{nullptr}; // 10.0. 0.0 to 10.255. 255.255. @@ -56,6 +50,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) hyperv::hcn::CreateNetworkParameters network_parameters{}; network_parameters.name = "multipass-hyperv-cit"; network_parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47486"; + network_parameters.flags = hyperv::hcn::HcnNetworkFlags::enable_dhcp_server; network_parameters.ipams = { hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; @@ -139,31 +134,11 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) ASSERT_TRUE(status.success()); } - // HCN assigns endpoint addresses asynchronously, so allow a bounded wait. - std::optional endpoint_ipv4; - for (auto attempts = 0; attempts < 20 && !endpoint_ipv4; ++attempts) - { - hyperv::hcn::HcnEndpointInfo endpoint_info; - const auto result = HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); - ASSERT_TRUE(result); - - for (const auto& ip_address : endpoint_info.ip_addresses) - { - try - { - endpoint_ipv4.emplace(ip_address); - break; - } - catch (const std::invalid_argument&) - { - // Ignore IPv6 configurations; IPAddress only represents IPv4. - } - } - - if (!endpoint_ipv4) - std::this_thread::sleep_for(250ms); - } - ASSERT_TRUE(endpoint_ipv4); + hyperv::hcn::HcnEndpointInfo endpoint_info; + const auto query_result = + HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); + ASSERT_TRUE(query_result); + EXPECT_TRUE(endpoint_info.ip_addresses.empty()); (void)HCS().terminate_compute_system(handle); handle.reset(); diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index b78d859b9c..a1b51ccf44 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -129,11 +129,12 @@ TEST_F(HyperVHCNAPI_IntegrationTests, query_nonexistent_network) EXPECT_FALSE(result); } -TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) +TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint_on_ics_dhcp_network) { CreateNetworkParameters network_params{}; network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + network_params.flags = HcnNetworkFlags::enable_dhcp_server; network_params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; CreateEndpointParameters endpoint_params{}; @@ -165,7 +166,7 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint(endpoint_params.endpoint_guid, endpoint_info); ASSERT_TRUE(result); - EXPECT_FALSE(endpoint_info.ip_addresses.empty()); + EXPECT_TRUE(endpoint_info.ip_addresses.empty()); } { diff --git a/tests/unit/windows/test_platform_win.cpp b/tests/unit/windows/test_platform_win.cpp index 9126787b46..918f2e137c 100644 --- a/tests/unit/windows/test_platform_win.cpp +++ b/tests/unit/windows/test_platform_win.cpp @@ -24,6 +24,8 @@ #include "tests/unit/mock_utils.h" #include "tests/unit/temp_dir.h" +#include "shared/windows/network_utils.h" + #include #include #include @@ -144,6 +146,11 @@ TEST(PlatformWin, noExtraDaemonSettings) EXPECT_THAT(MP_PLATFORM.extra_daemon_settings(), IsEmpty()); } +TEST(WindowsNetworkUtils, invalidMacHasNoPermanentIpv4Neighbor) +{ + EXPECT_FALSE(mp::permanent_ipv4_neighbor("not-a-mac")); +} + TEST(PlatformWin, testDefaultDriver) { EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "hyperv_api", "virtualbox")); From ea68ccfb089abeafba1fd1c4987e4e755d169b95 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 18:08:53 +0300 Subject: [PATCH 08/17] [hyperv-hcn] Pull MAC from endpoint info --- .../hyperv_api/hcn/hyperv_hcn_endpoint_info.h | 2 ++ .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 13 ++++++++++++- .../unit/hyperv_api/test_ut_hyperv_hcn_api.cpp | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h index 16bdfedadc..2d87fedabd 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include @@ -24,6 +25,7 @@ namespace multipass::hyperv::hcn { struct HcnEndpointInfo { + std::optional mac_address; std::vector ip_addresses; }; } // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index 0802c34220..8ec1459c56 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -338,10 +338,21 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, if (ec || !parsed.is_object()) return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; - auto addresses = endpoint_ip_addresses(parsed.as_object()); + const auto& endpoint_properties = parsed.as_object(); + auto addresses = endpoint_ip_addresses(endpoint_properties); if (!addresses) return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + std::optional mac_address; + if (const auto* value = endpoint_properties.if_contains("MacAddress")) + { + if (!value->is_string()) + return {E_UNEXPECTED, L"Failed to process JSON returned from the API"}; + + mac_address = value->as_string(); + } + + out_info.mac_address = std::move(mac_address); out_info.ip_addresses = std::move(*addresses); return {result, L""}; } diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index a0e683136b..2f9be8bc7d 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -808,7 +808,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_failure) TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) { static wchar_t endpoint_properties[] = - LR"({"IpConfigurations":[{"IpAddress":"172.20.1.2","PrefixLength":20},{"IpAddress":"fe80::1","PrefixLength":64}]})"; + LR"({"MacAddress":"52-54-00-E9-36-7E","IpConfigurations":[{"IpAddress":"172.20.1.2","PrefixLength":20},{"IpAddress":"fe80::1","PrefixLength":64}]})"; expect_endpoint_query(endpoint_properties); @@ -816,6 +816,8 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); ASSERT_TRUE(result); + ASSERT_TRUE(endpoint_info.mac_address); + EXPECT_EQ(*endpoint_info.mac_address, "52-54-00-E9-36-7E"); ASSERT_EQ(endpoint_info.ip_addresses.size(), 2); EXPECT_EQ(endpoint_info.ip_addresses[0], "172.20.1.2"); EXPECT_EQ(endpoint_info.ip_addresses[1], "fe80::1"); @@ -921,4 +923,18 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_properties) EXPECT_TRUE(endpoint_info.ip_addresses.empty()); } +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_rejects_malformed_mac_address) +{ + static wchar_t endpoint_properties[] = LR"({"MacAddress":42})"; + + expect_endpoint_query(endpoint_properties); + + hcn::HcnEndpointInfo endpoint_info; + const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); + + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(result.code), E_UNEXPECTED); + EXPECT_FALSE(endpoint_info.mac_address); +} + } // namespace multipass::test From 1b948791bc722bbe0ec46380456936199adfc77d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 18:16:06 +0300 Subject: [PATCH 09/17] [win] Use singleton for network_utils --- .../hyperv_api/hcs_virtual_machine.cpp | 13 +++++++ .../backends/shared/windows/network_utils.cpp | 17 ++++++-- .../backends/shared/windows/network_utils.h | 15 ++++++- .../test_ut_hyperv_hcs_virtual_machine.cpp | 39 +++++++++++++++++-- tests/unit/windows/mock_network_utils.h | 36 +++++++++++++++++ tests/unit/windows/test_platform_win.cpp | 2 +- 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 tests/unit/windows/mock_network_utils.h diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index c58e0ce57e..b25eb3f836 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -17,6 +17,8 @@ #include +#include + #include #include #include @@ -540,6 +542,17 @@ std::optional HCSVirtualMachine::management_ipv4() } } + if (endpoint_info.mac_address) + { + if (const auto ip_address = + windows_network_utils().permanent_ipv4_neighbor(*endpoint_info.mac_address)) + { + IPAddress address{*ip_address}; + mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); + return address; + } + } + mpl::debug(get_name(), "management_ipv4() > endpoint `{}` has no IPv4 configuration", endpoint_guid); diff --git a/src/platform/backends/shared/windows/network_utils.cpp b/src/platform/backends/shared/windows/network_utils.cpp index 69e6948164..0937685b7a 100644 --- a/src/platform/backends/shared/windows/network_utils.cpp +++ b/src/platform/backends/shared/windows/network_utils.cpp @@ -61,9 +61,18 @@ std::string ipv4_to_string(const IN_ADDR& address) } } // namespace -std::optional permanent_ipv4_neighbor(const std::string& mac_address) +WindowsNetworkUtils::WindowsNetworkUtils( + const Singleton::PrivatePass& pass) noexcept + : Singleton{pass} { - if (!utils::valid_mac_address(mac_address)) +} + +std::optional +WindowsNetworkUtils::permanent_ipv4_neighbor(const std::string& mac_address) const +{ + auto canonical_mac = mac_address; + std::ranges::replace(canonical_mac, '-', ':'); + if (!utils::valid_mac_address(canonical_mac)) { logging::error(log_category, "Invalid MAC address `{}`", mac_address); return std::nullopt; @@ -84,11 +93,11 @@ std::optional permanent_ipv4_neighbor(const std::string& mac_addres return std::nullopt; } - const auto matches = [&mac_address](const MIB_IPNET_ROW2& row) { + const auto matches = [&canonical_mac](const MIB_IPNET_ROW2& row) { return row.Address.si_family == AF_INET && row.State == NlnsPermanent && row.PhysicalAddressLength == ethernet_address_length && std::ranges::equal(canonical_mac_address(row.PhysicalAddress), - mac_address, + canonical_mac, [](unsigned char lhs, unsigned char rhs) { return std::tolower(lhs) == std::tolower(rhs); }); diff --git a/src/platform/backends/shared/windows/network_utils.h b/src/platform/backends/shared/windows/network_utils.h index 76bbbbc526..037ee8dbb2 100644 --- a/src/platform/backends/shared/windows/network_utils.h +++ b/src/platform/backends/shared/windows/network_utils.h @@ -17,10 +17,23 @@ #pragma once +#include + #include #include namespace multipass { -[[nodiscard]] std::optional permanent_ipv4_neighbor(const std::string& mac_address); +struct WindowsNetworkUtils : public Singleton +{ + WindowsNetworkUtils(const Singleton::PrivatePass&) noexcept; + + [[nodiscard]] virtual std::optional + permanent_ipv4_neighbor(const std::string& mac_address) const; +}; + +inline const WindowsNetworkUtils& windows_network_utils() +{ + return WindowsNetworkUtils::instance(); +} } diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 349a291371..6a53963183 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -34,6 +34,7 @@ #include "tests/unit/stub_status_monitor.h" #include "tests/unit/temp_dir.h" #include "tests/unit/temp_file.h" +#include "tests/unit/windows/mock_network_utils.h" namespace mp = multipass; namespace mpt = multipass::test; @@ -97,6 +98,10 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test mpt::MockVirtDiskWrapper::inject(); mpt::MockVirtDiskWrapper& mock_virtdisk = *mock_virtdisk_wrapper_injection.first; + mpt::MockWindowsNetworkUtils::GuardedMock mock_network_utils_injection = + mpt::MockWindowsNetworkUtils::inject(); + mpt::MockWindowsNetworkUtils& mock_network_utils = *mock_network_utils_injection.first; + inline static auto mock_handle_raw = reinterpret_cast(0xbadf00d); hcs_handle_t mock_handle{mock_handle_raw, [](void*) {}}; void* compute_system_callback_context{nullptr}; @@ -195,12 +200,14 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test Return(hcs_op_result_t{0, L""}))); } - void expect_endpoint_query(std::vector ip_addresses) + void expect_endpoint_query(std::vector ip_addresses, + std::optional mac_address = std::nullopt) { EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce( - [ip_addresses = std::move(ip_addresses)](const std::string&, - mhv::hcn::HcnEndpointInfo& endpoint_info) { + .WillOnce([ip_addresses = std::move(ip_addresses), + mac_address = std::move(mac_address)]( + const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + endpoint_info.mac_address = mac_address; endpoint_info.ip_addresses = ip_addresses; return hcs_op_result_t{0, L""}; }); @@ -628,6 +635,30 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_ EXPECT_EQ(uut->management_ipv4(), std::nullopt); } +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_uses_permanent_neighbor) +{ + default_open_success(); + expect_endpoint_query({}, "aa-bb-cc-dd-ee-ff"); + + auto uut = construct_vm(); + EXPECT_CALL(mock_network_utils, permanent_ipv4_neighbor("aa-bb-cc-dd-ee-ff")) + .WillOnce(Return(std::optional{"10.123.45.67"})); + + EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); +} + +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_neighbor) +{ + default_open_success(); + expect_endpoint_query({}, "aa-bb-cc-dd-ee-ff"); + + auto uut = construct_vm(); + EXPECT_CALL(mock_network_utils, permanent_ipv4_neighbor("aa-bb-cc-dd-ee-ff")) + .WillOnce(Return(std::nullopt)); + + EXPECT_EQ(uut->management_ipv4(), std::nullopt); +} + // --------------------------------------------------------- TEST_F(HyperVHCSVirtualMachine_UnitTests, update_state) diff --git a/tests/unit/windows/mock_network_utils.h b/tests/unit/windows/mock_network_utils.h new file mode 100644 index 0000000000..65139da815 --- /dev/null +++ b/tests/unit/windows/mock_network_utils.h @@ -0,0 +1,36 @@ +/* + * 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 version 3 as + * published by the Free Software Foundation. + * + * 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 "shared/windows/network_utils.h" +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ +struct MockWindowsNetworkUtils : public WindowsNetworkUtils +{ + using WindowsNetworkUtils::WindowsNetworkUtils; + + MOCK_METHOD(std::optional, + permanent_ipv4_neighbor, + (const std::string&), + (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockWindowsNetworkUtils, WindowsNetworkUtils); +}; +} diff --git a/tests/unit/windows/test_platform_win.cpp b/tests/unit/windows/test_platform_win.cpp index 918f2e137c..c5c504be54 100644 --- a/tests/unit/windows/test_platform_win.cpp +++ b/tests/unit/windows/test_platform_win.cpp @@ -148,7 +148,7 @@ TEST(PlatformWin, noExtraDaemonSettings) TEST(WindowsNetworkUtils, invalidMacHasNoPermanentIpv4Neighbor) { - EXPECT_FALSE(mp::permanent_ipv4_neighbor("not-a-mac")); + EXPECT_FALSE(mp::windows_network_utils().permanent_ipv4_neighbor("not-a-mac")); } TEST(PlatformWin, testDefaultDriver) From e7e761162743e717288e04c3e088693d4b9a81c9 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 18:28:25 +0300 Subject: [PATCH 10/17] [tests-bb] Verify permanent_ipv4_neighbor --- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 64 +++++++++++++++----- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 7bcc47ee80..85953fdb64 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -16,8 +16,11 @@ */ #include "hyperv_test_utils.h" +#include "multipass/test_data_path.h" #include "tests/unit/common.h" +#include + #include #include @@ -29,12 +32,17 @@ #include +#include +#include +#include + namespace multipass::test { using namespace hyperv::hcs; using hyperv::hcn::HCN; using hyperv::virtdisk::VirtDisk; +using namespace std::chrono_literals; // Component level big bang integration tests for Hyper-V HCN/HCS + virtdisk API's. // These tests ensure that the API's working together as expected. @@ -42,7 +50,7 @@ struct HyperV_ComponentIntegrationTests : public ::testing::Test { }; -TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network) +TEST_F(HyperV_ComponentIntegrationTests, alpine_vm_gets_permanent_neighbor_on_ics_dhcp_network) { hyperv::hcs::HcsSystemHandle handle{nullptr}; // 10.0. 0.0 to 10.255. 255.255. @@ -61,6 +69,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network hyperv::hcn::CreateEndpointParameters endpoint_parameters{}; endpoint_parameters.network_guid = network_parameters.guid; endpoint_parameters.endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029f"; + endpoint_parameters.mac_address = "52-54-00-E9-36-7E"; return endpoint_parameters; }(); @@ -75,25 +84,43 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network }); const auto temp_path = make_tempfile_path(".vhdx"); - - const hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{ - .size_in_bytes = (1024 * 1024) * 512, // 512 MiB - .path = temp_path, - .predecessor = {}}; + const auto cloud_init_iso_path = + std::filesystem::path{test_data_path} / "cloud-init" / "cloud-init.iso"; + { + std::ofstream output{static_cast(temp_path), + std::ios::binary}; + ASSERT_TRUE(output); + for (const auto suffix : {"aa", "ab", "ac"}) + { + const auto part = std::filesystem::path{test_data_path} / "cloud-vhdx" / + fmt::format("alpine.vhdx.part-{}", suffix); + std::ifstream input{part, std::ios::binary}; + ASSERT_TRUE(input); + output << input.rdbuf(); + } + } const auto network_adapter = [&endpoint_parameters]() { hyperv::hcs::HcsNetworkAdapter network_adapter{}; network_adapter.endpoint_guid = endpoint_parameters.endpoint_guid; - network_adapter.mac_address = "00-15-5D-9D-CF-69"; + network_adapter.mac_address = *endpoint_parameters.mac_address; return network_adapter; }(); - const auto create_vm_parameters = [&network_adapter]() { + const auto create_vm_parameters = [&network_adapter, &temp_path, &cloud_init_iso_path]() { hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; vm_parameters.name = "multipass-hyperv-cit-vm"; vm_parameters.processor_count = 1; vm_parameters.memory_size_mb = 512; vm_parameters.network_adapters.push_back(network_adapter); + vm_parameters.scsi_devices = { + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), + .name = "Primary disk", + .path = temp_path}, + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::Iso(), + .name = "Cloud-init ISO", + .path = cloud_init_iso_path, + .read_only = true}}; return vm_parameters; }(); @@ -102,6 +129,8 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network (void)HCS().terminate_compute_system(handle); handle.reset(); } + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); // Create the test network { @@ -115,17 +144,13 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network ASSERT_TRUE(status.success()); } - // Create the test VHDX (empty) - { - const auto& [status, status_msg] = VirtDisk().create_virtual_disk(create_disk_parameters); - ASSERT_TRUE(status.success()); - } - // Create test VM { const auto& [status, status_msg] = HCS().create_compute_system(create_vm_parameters, handle); ASSERT_TRUE(status.success()); + ASSERT_TRUE(HCS().grant_vm_access(create_vm_parameters.name, temp_path)); + ASSERT_TRUE(HCS().grant_vm_access(create_vm_parameters.name, cloud_init_iso_path)); } // Start test VM @@ -139,6 +164,17 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_on_ics_dhcp_network HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); ASSERT_TRUE(query_result); EXPECT_TRUE(endpoint_info.ip_addresses.empty()); + ASSERT_TRUE(endpoint_info.mac_address); + + std::optional neighbor_address; + for (auto attempts = 0; attempts < 120 && !neighbor_address; ++attempts) + { + neighbor_address = + windows_network_utils().permanent_ipv4_neighbor(*endpoint_info.mac_address); + if (!neighbor_address) + std::this_thread::sleep_for(500ms); + } + ASSERT_TRUE(neighbor_address); (void)HCS().terminate_compute_system(handle); handle.reset(); From 766a6ffbf8bf8ee43fbcd3d8b59b1bed3694381b Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 18:53:54 +0300 Subject: [PATCH 11/17] [hyperv-tests] Add tests exercising HCN path --- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 100 ++++++++++++++++++ .../hyperv_api/test_it_hyperv_hcn_api.cpp | 41 +++++++ 2 files changed, 141 insertions(+) diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 85953fdb64..236329f04e 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -18,9 +18,14 @@ #include "hyperv_test_utils.h" #include "multipass/test_data_path.h" #include "tests/unit/common.h" +#include "tests/unit/stub_availability_zone.h" +#include "tests/unit/stub_ssh_key_provider.h" +#include "tests/unit/stub_status_monitor.h" #include +#include + #include #include @@ -28,6 +33,7 @@ #include #include #include +#include #include #include @@ -183,6 +189,100 @@ TEST_F(HyperV_ComponentIntegrationTests, alpine_vm_gets_permanent_neighbor_on_ic cleanup.dismiss(); } +TEST_F(HyperV_ComponentIntegrationTests, hcs_vm_gets_host_assigned_ipv4_from_hcn) +{ + hyperv::hcs::HcsSystemHandle handle{nullptr}; + const auto network_parameters = []() { + hyperv::hcn::CreateNetworkParameters parameters{}; + parameters.name = "multipass-hyperv-hcn-ip-cit"; + parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47487"; + parameters.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), + {hyperv::hcn::HcnSubnet{"10.99.100.0/24"}}}}; + return parameters; + }(); + + const std::string vm_name{"multipass-hyperv-hcn-ip-cit-vm"}; + const std::string mac_address{"00:15:5d:9d:cf:69"}; + const hyperv::hcn::CreateEndpointParameters endpoint_parameters{ + .network_guid = network_parameters.guid, + .endpoint_guid = "db4bdbf0-dc14-407f-9780-00155d9dcf69", + .mac_address = "00-15-5D-9D-CF-69"}; + + auto cleanup = sg::make_scope_guard([&]() noexcept { + if (handle) + { + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); + }); + + if (HCS().open_compute_system(vm_name, handle)) + { + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); + + { + const auto& [status, status_msg] = HCN().create_network(network_parameters); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + { + const auto& [status, status_msg] = HCN().create_endpoint(endpoint_parameters); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + { + hyperv::hcs::CreateComputeSystemParameters parameters{}; + parameters.name = vm_name; + parameters.processor_count = 1; + parameters.memory_size_mb = 512; + + const auto& [status, status_msg] = HCS().create_compute_system(parameters, handle); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + StubAvailabilityZone zone; + StubSSHKeyProvider key_provider; + StubVMStatusMonitor monitor; + const VirtualMachineDescription description{ + 1, + MemorySize{"512M"}, + MemorySize{}, + vm_name, + zone.get_name(), + mac_address, + {}, + "", + {"", "", "", "", {}, {}}, + "", + {}, + {}, + {}, + {}}; + + { + hyperv::HCSVirtualMachine vm{network_parameters.guid, + description, + monitor, + key_provider, + zone, + {}}; + const auto address = vm.management_ipv4(); + + ASSERT_TRUE(address); + EXPECT_TRUE(Subnet{"10.99.100.0/24"}.contains(*address)); + } +} + TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_boot) { hyperv::hcs::HcsSystemHandle handle{nullptr}; diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index a1b51ccf44..e6c0483354 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -17,6 +17,8 @@ #include "tests/unit/common.h" +#include + #include #include #include @@ -183,6 +185,45 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint_on_ics_dhcp_network cleanup.dismiss(); } +TEST_F(HyperVHCNAPI_IntegrationTests, query_endpoint_returns_host_assigned_ipv4) +{ + CreateNetworkParameters network_params{}; + network_params.name = "multipass-hyperv-api-hcn-query-endpoint-test"; + network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + network_params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + CreateEndpointParameters endpoint_params{}; + endpoint_params.network_guid = network_params.guid; + endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; + + auto cleanup = sg::make_scope_guard([&]() noexcept { + (void)HCN().delete_endpoint(endpoint_params.endpoint_guid); + (void)HCN().delete_network(network_params.guid); + }); + + (void)HCN().delete_endpoint(endpoint_params.endpoint_guid); + (void)HCN().delete_network(network_params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(network_params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().create_endpoint(endpoint_params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + HcnEndpointInfo endpoint_info; + const auto result = HCN().query_endpoint(endpoint_params.endpoint_guid, endpoint_info); + + ASSERT_TRUE(result); + ASSERT_EQ(endpoint_info.ip_addresses.size(), 1); + EXPECT_TRUE(Subnet{"172.50.224.0/20"}.contains(IPAddress{endpoint_info.ip_addresses.front()})); +} + TEST_F(HyperVHCNAPI_IntegrationTests, create_endpoint_explicit_mac) { CreateNetworkParameters network_params{}; From f931e77b49dbe2282c64e5aeb8732a37deecaae2 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 19:04:32 +0300 Subject: [PATCH 12/17] [hcs-vm] Simplify management_ipv4 --- .../hyperv_api/hcs_virtual_machine.cpp | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index b25eb3f836..de24a884b8 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -528,13 +528,17 @@ std::optional HCSVirtualMachine::management_ipv4() return std::nullopt; } + auto make_ip_address = [this](const std::string& addr_str) { + IPAddress address{addr_str}; + mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); + return address; + }; + for (const auto& ip_address : endpoint_info.ip_addresses) { try { - IPAddress address{ip_address}; - mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); - return address; + return make_ip_address(ip_address); } catch (const std::invalid_argument&) { @@ -544,18 +548,13 @@ std::optional HCSVirtualMachine::management_ipv4() if (endpoint_info.mac_address) { - if (const auto ip_address = - windows_network_utils().permanent_ipv4_neighbor(*endpoint_info.mac_address)) + if (const auto ip_address = windows_network_utils().permanent_ipv4_neighbor( + *endpoint_info.mac_address)) { - IPAddress address{*ip_address}; - mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", address.as_string()); - return address; + return make_ip_address(*ip_address); } } - mpl::debug(get_name(), - "management_ipv4() > endpoint `{}` has no IPv4 configuration", - endpoint_guid); return std::nullopt; } From 14c17595d7b200ca1b7e258be120a161cebff6e5 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 19:43:46 +0300 Subject: [PATCH 13/17] [ip-address] Stricter IP address parsing --- src/network/ip_address.cpp | 17 +++++++++-------- tests/unit/test_ip_address.cpp | 1 + tests/unit/test_subnet.cpp | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/network/ip_address.cpp b/src/network/ip_address.cpp index fd7311f4e8..cc601f3170 100644 --- a/src/network/ip_address.cpp +++ b/src/network/ip_address.cpp @@ -17,6 +17,7 @@ #include +#include #include #include @@ -38,18 +39,18 @@ bool is_valid_octet(int value) std::array parse(const std::string& ip) { - char ch; - int a = -1; - int b = -1; - int c = -1; - int d = -1; + // FIXME: Use Boost.ASIO? + std::array sep = {'\0', '\0', '\0'}; + std::array octets = {(int)-1, -1, -1, -1}; + std::stringstream s(ip); - s >> a >> ch >> b >> ch >> c >> ch >> d; + s >> octets[0] >> sep[0] >> octets[1] >> sep[1] >> octets[2] >> sep[2] >> octets[3]; - if (!is_valid_octet(a) || !is_valid_octet(b) || !is_valid_octet(c) || !is_valid_octet(d)) + if (!std::ranges::all_of(octets, is_valid_octet) || + !std::ranges::all_of(sep, [](char c) { return c == '.'; })) throw std::invalid_argument(fmt::format("invalid IP address {}", ip)); - return {{as_octet(a), as_octet(b), as_octet(c), as_octet(d)}}; + return {{as_octet(octets[0]), as_octet(octets[1]), as_octet(octets[2]), as_octet(octets[3])}}; } std::array to_octets(uint32_t value) diff --git a/tests/unit/test_ip_address.cpp b/tests/unit/test_ip_address.cpp index 6a4bbeac7c..fec29deb00 100644 --- a/tests/unit/test_ip_address.cpp +++ b/tests/unit/test_ip_address.cpp @@ -45,6 +45,7 @@ TEST(IPAddress, throwsOnInvalidIpString) EXPECT_THROW(mp::IPAddress ip{"256.256.256.256"}, std::invalid_argument); EXPECT_THROW(mp::IPAddress ip{"-2.-3.-5.-6"}, std::invalid_argument); EXPECT_THROW(mp::IPAddress ip{"a.b.c.d"}, std::invalid_argument); + EXPECT_THROW(mp::IPAddress ip{"1:2:3:4:5:6:7:8"}, std::invalid_argument); } TEST(IPAddress, canBeConvertedToInteger) diff --git a/tests/unit/test_subnet.cpp b/tests/unit/test_subnet.cpp index b0310e496f..2501a848ae 100644 --- a/tests/unit/test_subnet.cpp +++ b/tests/unit/test_subnet.cpp @@ -104,13 +104,13 @@ TEST(SubnetTest, givesCorrectRange) subnet = mp::Subnet{"121.212.1.152/11"}; EXPECT_EQ(subnet.masked_address(), mp::IPAddress{"121.192.0.0"}); EXPECT_EQ(subnet.min_address(), mp::IPAddress{"121.192.0.1"}); - EXPECT_EQ(subnet.max_address(), mp::IPAddress{"121,223.255.254"}); + EXPECT_EQ(subnet.max_address(), mp::IPAddress{"121.223.255.254"}); EXPECT_EQ(subnet.usable_address_count(), 2097150); subnet = mp::Subnet{"0.0.0.0/0"}; EXPECT_EQ(subnet.masked_address(), mp::IPAddress{"0.0.0.0"}); EXPECT_EQ(subnet.min_address(), mp::IPAddress{"0.0.0.1"}); - EXPECT_EQ(subnet.max_address(), mp::IPAddress{"255,255.255.254"}); + EXPECT_EQ(subnet.max_address(), mp::IPAddress{"255.255.255.254"}); EXPECT_EQ(subnet.usable_address_count(), 4294967294); } From 50ea88faafe9eb673ceb8b377457f7079d37c9ca Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 19:44:43 +0300 Subject: [PATCH 14/17] [hyperv-hcn] Simplify code --- .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 18 ++++++++++-------- .../backends/shared/windows/network_utils.cpp | 9 ++++----- .../unit/hyperv_api/test_it_hyperv_hcn_api.cpp | 3 +-- .../unit/hyperv_api/test_ut_hyperv_hcn_api.cpp | 5 ++++- .../test_ut_hyperv_hcs_virtual_machine.cpp | 12 +----------- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index 8ec1459c56..1aa8d4c1f3 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -319,13 +319,15 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, if (!open_result) return open_result; - UniqueCotaskmemString properties{}, result_msgbuf{}; - const auto result = ResultCode{API().HcnQueryEndpointProperties(endpoint.get(), - L"{}", - out_ptr(properties), - out_ptr(result_msgbuf))}; - if (!result.success()) - return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; + UniqueCotaskmemString properties{}; + const auto result = perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnQueryEndpointProperties(endpoint.get(), + L"{}", + out_ptr(properties), + rmsgbuf); + }); + if (!result) + return result; if (!properties) return {E_UNEXPECTED, L"HCN returned no endpoint properties"}; @@ -354,7 +356,7 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, out_info.mac_address = std::move(mac_address); out_info.ip_addresses = std::move(*addresses); - return {result, L""}; + return result; } // --------------------------------------------------------- diff --git a/src/platform/backends/shared/windows/network_utils.cpp b/src/platform/backends/shared/windows/network_utils.cpp index 0937685b7a..be7d3fa47b 100644 --- a/src/platform/backends/shared/windows/network_utils.cpp +++ b/src/platform/backends/shared/windows/network_utils.cpp @@ -77,6 +77,9 @@ WindowsNetworkUtils::permanent_ipv4_neighbor(const std::string& mac_address) con logging::error(log_category, "Invalid MAC address `{}`", mac_address); return std::nullopt; } + std::ranges::transform(canonical_mac, canonical_mac.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); PMIB_IPNET_TABLE2 raw_table{}; if (const auto result = GetIpNetTable2(AF_INET, &raw_table); result != NO_ERROR) @@ -96,11 +99,7 @@ WindowsNetworkUtils::permanent_ipv4_neighbor(const std::string& mac_address) con const auto matches = [&canonical_mac](const MIB_IPNET_ROW2& row) { return row.Address.si_family == AF_INET && row.State == NlnsPermanent && row.PhysicalAddressLength == ethernet_address_length && - std::ranges::equal(canonical_mac_address(row.PhysicalAddress), - canonical_mac, - [](unsigned char lhs, unsigned char rhs) { - return std::tolower(lhs) == std::tolower(rhs); - }); + canonical_mac_address(row.PhysicalAddress) == canonical_mac; }; const auto* begin = table->Table; diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index e6c0483354..12f4996ac0 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -131,7 +131,7 @@ TEST_F(HyperVHCNAPI_IntegrationTests, query_nonexistent_network) EXPECT_FALSE(result); } -TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint_on_ics_dhcp_network) +TEST_F(HyperVHCNAPI_IntegrationTests, ics_dhcp_endpoint_has_no_host_assigned_ip) { CreateNetworkParameters network_params{}; network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; @@ -159,7 +159,6 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint_on_ics_dhcp_network { const auto& [status, error_msg] = HCN().create_endpoint(endpoint_params); - std::wprintf(L"%s\n", error_msg.c_str()); ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp index 2f9be8bc7d..114e59c5ed 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -98,7 +98,8 @@ struct HyperVHCNAPI_UnitTests : public ::testing::Test mpl::Level::trace, "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); logger_scope.mock_logger->expect_log(mpl::Level::trace, - "perform_hcn_operation(...) > result: true"); + "perform_hcn_operation(...) > result: true", + testing::Exactly(2)); logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); } }; @@ -871,6 +872,8 @@ TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_query_failure) "open_endpoint(...) > endpoint_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); logger_scope.mock_logger->expect_log(mpl::Level::trace, "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); hcn::HcnEndpointInfo endpoint_info; const auto result = HCN().query_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81", endpoint_info); diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 6a53963183..0fcb6cec20 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -568,7 +568,7 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname_throws_when_ip_is_unav TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_primary_endpoint) { default_open_success(); - expect_endpoint_query({"fe80::1", "10.123.45.67"}); + expect_endpoint_query({"fe80::1", "1:2:3:4:5:6:7:8", "10.123.45.67"}); auto uut = construct_vm(); @@ -615,16 +615,6 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_retries_unsuccessful_q EXPECT_EQ(uut->management_ipv4(), mp::IPAddress{"10.123.45.67"}); } -TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_when_query_fails) -{ - default_open_success(); - expect_endpoint_query_failure(); - - auto uut = construct_vm(); - - EXPECT_EQ(uut->management_ipv4(), std::nullopt); -} - TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_ipv4_configuration) { default_open_success(); From 3c9be082d6871581dab86be7f99d419349f5dd0a Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 19:48:37 +0300 Subject: [PATCH 15/17] [hyperv-hcn] Format code changes --- .../hyperv_api/hcn/hyperv_hcn_wrapper.cpp | 2 + .../backends/shared/windows/network_utils.cpp | 8 ++-- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 46 +++++++++---------- .../test_ut_hyperv_hcs_virtual_machine.cpp | 7 +-- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp index 1aa8d4c1f3..40f420d614 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -315,6 +315,8 @@ OperationResult HCNWrapper::query_endpoint(const std::string& endpoint_guid, { mpl::trace(log_category, "HCNWrapper::query_endpoint(...) > endpoint_guid: {}", endpoint_guid); + out_info = {}; + const auto& [open_result, endpoint] = open_endpoint(endpoint_guid); if (!open_result) return open_result; diff --git a/src/platform/backends/shared/windows/network_utils.cpp b/src/platform/backends/shared/windows/network_utils.cpp index be7d3fa47b..d89185bc4a 100644 --- a/src/platform/backends/shared/windows/network_utils.cpp +++ b/src/platform/backends/shared/windows/network_utils.cpp @@ -22,9 +22,9 @@ #include -#include -#include #include +#include +#include #include #include #include @@ -67,8 +67,8 @@ WindowsNetworkUtils::WindowsNetworkUtils( { } -std::optional -WindowsNetworkUtils::permanent_ipv4_neighbor(const std::string& mac_address) const +std::optional WindowsNetworkUtils::permanent_ipv4_neighbor( + const std::string& mac_address) const { auto canonical_mac = mac_address; std::ranges::replace(canonical_mac, '-', ':'); diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 236329f04e..3aa667e077 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -90,8 +90,8 @@ TEST_F(HyperV_ComponentIntegrationTests, alpine_vm_gets_permanent_neighbor_on_ic }); const auto temp_path = make_tempfile_path(".vhdx"); - const auto cloud_init_iso_path = - std::filesystem::path{test_data_path} / "cloud-init" / "cloud-init.iso"; + const auto cloud_init_iso_path = std::filesystem::path{test_data_path} / "cloud-init" / + "cloud-init.iso"; { std::ofstream output{static_cast(temp_path), std::ios::binary}; @@ -166,8 +166,8 @@ TEST_F(HyperV_ComponentIntegrationTests, alpine_vm_gets_permanent_neighbor_on_ic } hyperv::hcn::HcnEndpointInfo endpoint_info; - const auto query_result = - HCN().query_endpoint(endpoint_parameters.endpoint_guid, endpoint_info); + const auto query_result = HCN().query_endpoint(endpoint_parameters.endpoint_guid, + endpoint_info); ASSERT_TRUE(query_result); EXPECT_TRUE(endpoint_info.ip_addresses.empty()); ASSERT_TRUE(endpoint_info.mac_address); @@ -175,8 +175,8 @@ TEST_F(HyperV_ComponentIntegrationTests, alpine_vm_gets_permanent_neighbor_on_ic std::optional neighbor_address; for (auto attempts = 0; attempts < 120 && !neighbor_address; ++attempts) { - neighbor_address = - windows_network_utils().permanent_ipv4_neighbor(*endpoint_info.mac_address); + neighbor_address = windows_network_utils().permanent_ipv4_neighbor( + *endpoint_info.mac_address); if (!neighbor_address) std::this_thread::sleep_for(500ms); } @@ -196,9 +196,8 @@ TEST_F(HyperV_ComponentIntegrationTests, hcs_vm_gets_host_assigned_ipv4_from_hcn hyperv::hcn::CreateNetworkParameters parameters{}; parameters.name = "multipass-hyperv-hcn-ip-cit"; parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47487"; - parameters.ipams = { - hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), - {hyperv::hcn::HcnSubnet{"10.99.100.0/24"}}}}; + parameters.ipams = {hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), + {hyperv::hcn::HcnSubnet{"10.99.100.0/24"}}}}; return parameters; }(); @@ -253,21 +252,20 @@ TEST_F(HyperV_ComponentIntegrationTests, hcs_vm_gets_host_assigned_ipv4_from_hcn StubAvailabilityZone zone; StubSSHKeyProvider key_provider; StubVMStatusMonitor monitor; - const VirtualMachineDescription description{ - 1, - MemorySize{"512M"}, - MemorySize{}, - vm_name, - zone.get_name(), - mac_address, - {}, - "", - {"", "", "", "", {}, {}}, - "", - {}, - {}, - {}, - {}}; + const VirtualMachineDescription description{1, + MemorySize{"512M"}, + MemorySize{}, + vm_name, + zone.get_name(), + mac_address, + {}, + "", + {"", "", "", "", {}, {}}, + "", + {}, + {}, + {}, + {}}; { hyperv::HCSVirtualMachine vm{network_parameters.guid, diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 0fcb6cec20..1d151a9d0c 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -204,9 +204,10 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test std::optional mac_address = std::nullopt) { EXPECT_CALL(mock_hcn, query_endpoint(Eq("db4bdbf0-dc14-407f-9780-aabbccddeeff"), _)) - .WillOnce([ip_addresses = std::move(ip_addresses), - mac_address = std::move(mac_address)]( - const std::string&, mhv::hcn::HcnEndpointInfo& endpoint_info) { + .WillOnce( + [ip_addresses = std::move(ip_addresses), + mac_address = std::move(mac_address)](const std::string&, + mhv::hcn::HcnEndpointInfo& endpoint_info) { endpoint_info.mac_address = mac_address; endpoint_info.ip_addresses = ip_addresses; return hcs_op_result_t{0, L""}; From f7803664091ce1df7bb89661acc2025f82d1e8d5 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 20:00:27 +0300 Subject: [PATCH 16/17] [hyperv-hcn] Revert test change --- .../unit/hyperv_api/test_it_hyperv_hcn_api.cpp | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index 12f4996ac0..7fe74b4625 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -131,12 +131,11 @@ TEST_F(HyperVHCNAPI_IntegrationTests, query_nonexistent_network) EXPECT_FALSE(result); } -TEST_F(HyperVHCNAPI_IntegrationTests, ics_dhcp_endpoint_has_no_host_assigned_ip) +TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) { CreateNetworkParameters network_params{}; network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; - network_params.flags = HcnNetworkFlags::enable_dhcp_server; network_params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; CreateEndpointParameters endpoint_params{}; @@ -144,11 +143,6 @@ TEST_F(HyperVHCNAPI_IntegrationTests, ics_dhcp_endpoint_has_no_host_assigned_ip) endpoint_params.network_guid = network_params.guid; endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; - auto cleanup = sg::make_scope_guard([&]() noexcept { - (void)HCN().delete_endpoint(endpoint_params.endpoint_guid); - (void)HCN().delete_network(network_params.guid); - }); - (void)HCN().delete_network(network_params.guid); { @@ -159,17 +153,11 @@ TEST_F(HyperVHCNAPI_IntegrationTests, ics_dhcp_endpoint_has_no_host_assigned_ip) { const auto& [status, error_msg] = HCN().create_endpoint(endpoint_params); + std::wprintf(L"%s\n", error_msg.c_str()); ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } - { - HcnEndpointInfo endpoint_info; - const auto result = HCN().query_endpoint(endpoint_params.endpoint_guid, endpoint_info); - ASSERT_TRUE(result); - EXPECT_TRUE(endpoint_info.ip_addresses.empty()); - } - { const auto& [status, error_msg] = HCN().delete_endpoint(endpoint_params.endpoint_guid); ASSERT_TRUE(status.success()); @@ -181,7 +169,6 @@ TEST_F(HyperVHCNAPI_IntegrationTests, ics_dhcp_endpoint_has_no_host_assigned_ip) ASSERT_TRUE(status.success()); ASSERT_TRUE(error_msg.empty()); } - cleanup.dismiss(); } TEST_F(HyperVHCNAPI_IntegrationTests, query_endpoint_returns_host_assigned_ipv4) From a42633045dbb9a815b58e601a833e0c087d11640 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 22 Jul 2026 20:05:45 +0300 Subject: [PATCH 17/17] [hyperv-test-bb] Reintroduce empty vm test --- .../backends/shared/windows/network_utils.h | 6 +- tests/unit/hyperv_api/test_bb_cit_hyperv.cpp | 87 +++++++++++++++++++ tests/unit/windows/mock_network_utils.h | 2 +- 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/platform/backends/shared/windows/network_utils.h b/src/platform/backends/shared/windows/network_utils.h index 037ee8dbb2..25224b2cc0 100644 --- a/src/platform/backends/shared/windows/network_utils.h +++ b/src/platform/backends/shared/windows/network_utils.h @@ -28,12 +28,12 @@ struct WindowsNetworkUtils : public Singleton { WindowsNetworkUtils(const Singleton::PrivatePass&) noexcept; - [[nodiscard]] virtual std::optional - permanent_ipv4_neighbor(const std::string& mac_address) const; + [[nodiscard]] virtual std::optional permanent_ipv4_neighbor( + const std::string& mac_address) const; }; inline const WindowsNetworkUtils& windows_network_utils() { return WindowsNetworkUtils::instance(); } -} +} // namespace multipass diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index 3aa667e077..030305851b 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -281,6 +281,93 @@ TEST_F(HyperV_ComponentIntegrationTests, hcs_vm_gets_host_assigned_ipv4_from_hcn } } +TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) +{ + hyperv::hcs::HcsSystemHandle handle{nullptr}; + // 10.0. 0.0 to 10.255. 255.255. + const auto network_parameters = []() { + hyperv::hcn::CreateNetworkParameters network_parameters{}; + network_parameters.name = "multipass-hyperv-cit"; + network_parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47486"; + network_parameters.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), + {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; + return network_parameters; + }(); + + const auto endpoint_parameters = [&network_parameters]() { + hyperv::hcn::CreateEndpointParameters endpoint_parameters{}; + endpoint_parameters.network_guid = network_parameters.guid; + endpoint_parameters.endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029f"; + return endpoint_parameters; + }(); + + const auto temp_path = make_tempfile_path(".vhdx"); + + const hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{ + .size_in_bytes = (1024 * 1024) * 512, // 512 MiB + .path = temp_path, + .predecessor = {}}; + + const auto network_adapter = [&endpoint_parameters]() { + hyperv::hcs::HcsNetworkAdapter network_adapter{}; + network_adapter.endpoint_guid = endpoint_parameters.endpoint_guid; + network_adapter.mac_address = "00-15-5D-9D-CF-69"; + return network_adapter; + }(); + + const auto create_vm_parameters = [&network_adapter]() { + hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; + vm_parameters.name = "multipass-hyperv-cit-vm"; + vm_parameters.processor_count = 1; + vm_parameters.memory_size_mb = 512; + vm_parameters.network_adapters.push_back(network_adapter); + return vm_parameters; + }(); + + if (HCS().open_compute_system(create_vm_parameters.name, handle)) + { + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } + + // Create the test network + { + const auto& [status, status_msg] = HCN().create_network(network_parameters); + ASSERT_TRUE(status.success()); + } + + // Create the test endpoint + { + const auto& [status, status_msg] = HCN().create_endpoint(endpoint_parameters); + ASSERT_TRUE(status.success()); + } + + // Create the test VHDX (empty) + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(create_disk_parameters); + ASSERT_TRUE(status.success()); + } + + // Create test VM + { + const auto& [status, status_msg] = + HCS().create_compute_system(create_vm_parameters, handle); + ASSERT_TRUE(status.success()); + } + + // Start test VM + { + const auto& [status, status_msg] = HCS().start_compute_system(handle); + ASSERT_TRUE(status.success()); + } + + (void)HCS().terminate_compute_system(handle); + handle.reset(); + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); +} + TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_boot) { hyperv::hcs::HcsSystemHandle handle{nullptr}; diff --git a/tests/unit/windows/mock_network_utils.h b/tests/unit/windows/mock_network_utils.h index 65139da815..dbed916c18 100644 --- a/tests/unit/windows/mock_network_utils.h +++ b/tests/unit/windows/mock_network_utils.h @@ -33,4 +33,4 @@ struct MockWindowsNetworkUtils : public WindowsNetworkUtils MP_MOCK_SINGLETON_BOILERPLATE(MockWindowsNetworkUtils, WindowsNetworkUtils); }; -} +} // namespace multipass::test