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/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..2d87fedabd --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_info.h @@ -0,0 +1,31 @@ +/* + * 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 +#include + +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 ab8670cdca..40f420d614 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 @@ -186,6 +187,52 @@ 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)); +} + +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 // --------------------------------------------------------- @@ -263,6 +310,59 @@ 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); + + out_info = {}; + + const auto& [open_result, endpoint] = open_endpoint(endpoint_guid); + if (!open_result) + return open_result; + + 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"}; + + 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"}; + + 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; +} + +// --------------------------------------------------------- + OperationResult HCNWrapper::enumerate_attached_endpoints( const std::string& vm_guid, std::vector& endpoint_guids) const @@ -360,8 +460,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/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/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index f7b8e1503f..de24a884b8 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 @@ -26,7 +28,6 @@ #include #include -#include #include #include @@ -40,8 +41,6 @@ #include -#include - namespace { @@ -69,97 +68,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) @@ -356,8 +264,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)) { @@ -388,12 +296,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(), @@ -402,8 +310,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 {}", @@ -600,7 +508,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() { @@ -609,19 +517,45 @@ 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(); + 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 + { + return make_ip_address(ip_address); + } + catch (const std::invalid_argument&) + { + // HCN also reports IPv6 configurations, which IPAddress does not represent. + } + } - mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", result); + if (endpoint_info.mac_address) + { + if (const auto ip_address = windows_network_utils().permanent_ipv4_neighbor( + *endpoint_info.mac_address)) + { + return make_ip_address(*ip_address); + } + } - // Prefer the first one - return std::make_optional(result); + return std::nullopt; } void HCSVirtualMachine::handle_state_update() @@ -652,8 +586,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/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..d89185bc4a --- /dev/null +++ b/src/platform/backends/shared/windows/network_utils.cpp @@ -0,0 +1,113 @@ +/* + * 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 + +WindowsNetworkUtils::WindowsNetworkUtils( + const Singleton::PrivatePass& pass) noexcept + : Singleton{pass} +{ +} + +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; + } + 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) + { + 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 = [&canonical_mac](const MIB_IPNET_ROW2& row) { + return row.Address.si_family == AF_INET && row.State == NlnsPermanent && + row.PhysicalAddressLength == ethernet_address_length && + canonical_mac_address(row.PhysicalAddress) == canonical_mac; + }; + + 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..25224b2cc0 --- /dev/null +++ b/src/platform/backends/shared/windows/network_utils.h @@ -0,0 +1,39 @@ +/* + * 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 +#include + +namespace multipass +{ +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(); +} +} // namespace multipass 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_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp index b85ac632c7..030305851b 100644 --- a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -16,22 +16,39 @@ */ #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 #include +#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. @@ -39,6 +56,231 @@ struct HyperV_ComponentIntegrationTests : public ::testing::Test { }; +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. + 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.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"}}}}; + 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"; + endpoint_parameters.mac_address = "52-54-00-E9-36-7E"; + 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 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 = *endpoint_parameters.mac_address; + return 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; + }(); + + if (HCS().open_compute_system(create_vm_parameters.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); + + // 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 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 + { + const auto& [status, status_msg] = HCS().start_compute_system(handle); + ASSERT_TRUE(status.success()); + } + + 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()); + 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(); + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); + 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) { hyperv::hcs::HcsSystemHandle handle{nullptr}; @@ -217,8 +459,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()); } @@ -239,8 +481,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()); } @@ -249,11 +491,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_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp index e6b45394c2..7fe74b4625 100644 --- a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -17,11 +17,16 @@ #include "tests/unit/common.h" +#include + #include #include #include +#include #include +#include + namespace multipass::test { @@ -166,6 +171,45 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) } } +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{}; 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..114e59c5ed 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 @@ -62,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", + testing::Exactly(2)); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "query_endpoint result:"); + } }; // --------------------------------------------------------- @@ -461,8 +501,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()); } @@ -497,8 +537,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); @@ -724,8 +764,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()); } @@ -756,12 +796,148 @@ 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); } } +// --------------------------------------------------------- + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_success) +{ + static wchar_t endpoint_properties[] = + 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); + + hcn::HcnEndpointInfo endpoint_info; + 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"); +} + +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"); + 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_accepts_unassigned_ip) +{ + static wchar_t endpoint_properties[] = LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81"})"; + + 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_addresses.empty()); +} + +TEST_F(HyperVHCNAPI_UnitTests, query_endpoint_merges_flattened_ip_configuration) +{ + static wchar_t endpoint_properties[] = + LR"({"ID":"af3fb745-2f23-463c-8ded-443f876d9e81","IpConfigurations":[{"IpAddress":"fe80::1","PrefixLength":64}],"IPAddress":"172.20.1.2","PrefixLength":20})"; + + 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_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_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_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 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..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 @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -33,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; @@ -96,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}; @@ -194,6 +200,26 @@ struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test Return(hcs_op_result_t{0, L""}))); } + 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), + 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""}; + }); + } + + 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) { @@ -521,10 +547,107 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_port) TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) { default_open_success(); + expect_endpoint_query({"10.123.45.67"}); - std::shared_ptr uut{nullptr}; - ASSERT_NO_THROW(uut = construct_vm()); - EXPECT_EQ(uut->ssh_hostname(), uut->get_name() + ".mshome.net"); + 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_endpoint_query_failure(); + + auto uut = construct_vm(); + + EXPECT_THROW((void)uut->ssh_hostname(), mp::IPUnavailableException); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_queries_primary_endpoint) +{ + default_open_success(); + expect_endpoint_query({"fe80::1", "1:2:3:4:5:6:7:8", "10.123.45.67"}); + + auto uut = construct_vm(); + + 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_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_addresses = {"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_addresses = {"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_without_ipv4_configuration) +{ + default_open_success(); + expect_endpoint_query({"fe80::1"}); + + auto uut = construct_vm(); + + 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); } // --------------------------------------------------------- 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); } diff --git a/tests/unit/windows/mock_network_utils.h b/tests/unit/windows/mock_network_utils.h new file mode 100644 index 0000000000..dbed916c18 --- /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); +}; +} // namespace multipass::test diff --git a/tests/unit/windows/test_platform_win.cpp b/tests/unit/windows/test_platform_win.cpp index 9126787b46..c5c504be54 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::windows_network_utils().permanent_ipv4_neighbor("not-a-mac")); +} + TEST(PlatformWin, testDefaultDriver) { EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "hyperv_api", "virtualbox"));