From 9fbac275e827eb8c74a3ce736d39f9df9e3c9660 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 18 Feb 2025 19:21:37 +0300 Subject: [PATCH 01/76] [hyperv-hcn] remove the "IpConfigurations" section from create endpoint settings It's a remnant from the original WSL2 traces. It works just as fine without the IpConfigurations settings. --- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 12 +- .../hcn/hyperv_hcn_create_endpoint_params.h | 10 +- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 23 ++++ tests/hyperv_api/test_bb_cit_hyperv.cpp | 108 ++++++++++++++++-- tests/hyperv_api/test_it_hyperv_hcn_api.cpp | 1 - tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 19 +-- 6 files changed, 128 insertions(+), 45 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 7a5394bff2..e23ff2a382 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -225,19 +225,11 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para "Minor": 16 }}, "HostComputeNetwork": "{0}", - "Policies": [ - ], - "IpConfigurations": [ - {{ - "IpAddress": "{1}" - }} - ] + "Policies": [] }})"; // Render the template - const auto endpoint_settings = fmt::format(endpoint_settings_template, - string_to_wstring(params.network_guid), - string_to_wstring(params.endpoint_ipvx_addr)); + const auto endpoint_settings = fmt::format(endpoint_settings_template, string_to_wstring(params.network_guid)); HCN_ENDPOINT endpoint{nullptr}; const auto result = perform_hcn_operation(api, api.CreateEndpoint, diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h index 478f401da0..2ad5030a80 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h @@ -42,11 +42,6 @@ struct CreateEndpointParameters * Must be unique. */ std::string endpoint_guid{}; - - /** - * The IPv[4-6] address to assign to the endpoint. - */ - std::string endpoint_ipvx_addr{}; }; } // namespace multipass::hyperv::hcn @@ -66,10 +61,9 @@ struct fmt::formatter auto format(const multipass::hyperv::hcn::CreateEndpointParameters& params, FormatContext& ctx) const { return format_to(ctx.out(), - "Endpoint GUID: ({}) | Network GUID: ({}) | Endpoint IPvX Addr.: ({})", + "Endpoint GUID: ({}) | Network GUID: ({})", params.endpoint_guid, - params.network_guid, - params.endpoint_ipvx_addr); + params.network_guid); } }; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 34c6482e87..ebff4de5d0 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -255,6 +255,29 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam return result; }(); + const auto network_adapters = [&]() { + std::wstring result = {}; + + constexpr auto network_adapter_template = LR"( + "{0}": {{ + "EndpointId" : "{0}", + "MacAddress": "{1}" + }},)"; + + for (const auto& endpoint : params.endpoints) + { + result += fmt::format(network_adapter_template, + string_to_wstring(endpoint.endpoint_guid), + string_to_wstring(endpoint.nic_mac_address)); + } + + // Remove the last comma. + if (!result.empty()) + result.pop_back(); + + return result; + }(); + // Ideally, we should codegen from the schema // and use that. // https://raw.githubusercontent.com/MicrosoftDocs/Virtualization-Documentation/refs/heads/main/hyperv-samples/hcs-samples/JSON_files/HCS_Schema%5BWindows_10_SDK_version_1809%5D.json diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index 1fa8740676..eb49f39e79 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -58,7 +58,6 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) hyperv::hcn::CreateEndpointParameters endpoint_parameters{}; endpoint_parameters.network_guid = network_parameters.guid; endpoint_parameters.endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029f"; - endpoint_parameters.endpoint_ipvx_addr = "10.99.99.10"; return endpoint_parameters; }(); @@ -71,7 +70,104 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) return create_disk_parameters; }(); - const auto create_vm_parameters = []() { + const auto add_endpoint_parameters = [&endpoint_parameters]() { + hyperv::hcs::AddEndpointParameters add_endpoint_parameters{}; + add_endpoint_parameters.endpoint_guid = endpoint_parameters.endpoint_guid; + add_endpoint_parameters.target_compute_system_name = "multipass-hyperv-cit-vm"; + add_endpoint_parameters.nic_mac_address = "00-15-5D-9D-CF-69"; + return add_endpoint_parameters; + }(); + + const auto create_vm_parameters = [&add_endpoint_parameters]() { + 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.endpoints.push_back(add_endpoint_parameters); + return vm_parameters; + }(); + + (void)hcs.terminate_compute_system(create_vm_parameters.name); + + // Create the test network + { + const auto& [status, status_msg] = hcn.create_network(network_parameters); + ASSERT_TRUE(status); + } + + // Create the test endpoint + { + const auto& [status, status_msg] = hcn.create_endpoint(endpoint_parameters); + ASSERT_TRUE(status); + } + + // Create the test VHDX (empty) + { + const auto& [status, status_msg] = virtdisk.create_virtual_disk(create_disk_parameters); + ASSERT_TRUE(status); + } + + // Create test VM + { + const auto& [status, status_msg] = hcs.create_compute_system(create_vm_parameters); + ASSERT_TRUE(status); + } + + // Start test VM + { + const auto& [status, status_msg] = hcs.start_compute_system(create_vm_parameters.name); + ASSERT_TRUE(status); + } + + (void)hcs.terminate_compute_system(create_vm_parameters.name); + (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) +{ + hcn_wrapper_t hcn{}; + hcs_wrapper_t hcs{}; + virtdisk_wrapper_t virtdisk{}; + // 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.subnet = "10.99.99.0/24"; + network_parameters.gateway = "10.99.99.1"; + 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; + }(); + + // Remove remnants from previous tests, if any. + (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 create_disk_parameters = [&temp_path]() { + hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{}; + create_disk_parameters.path = temp_path; + create_disk_parameters.size_in_bytes = (1024 * 1024) * 512; // 512 MiB + return create_disk_parameters; + }(); + + const auto add_endpoint_parameters = [&endpoint_parameters]() { + hyperv::hcs::AddEndpointParameters add_endpoint_parameters{}; + add_endpoint_parameters.endpoint_guid = endpoint_parameters.endpoint_guid; + add_endpoint_parameters.target_compute_system_name = "multipass-hyperv-cit-vm"; + add_endpoint_parameters.nic_mac_address = "00-15-5D-9D-CF-69"; + return add_endpoint_parameters; + }(); + + const auto create_vm_parameters = [&add_endpoint_parameters]() { hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; vm_parameters.name = "multipass-hyperv-cit-vm"; vm_parameters.processor_count = 1; @@ -96,14 +192,6 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) } } - const auto add_endpoint_parameters = [&create_vm_parameters, &endpoint_parameters]() { - hyperv::hcs::AddEndpointParameters add_endpoint_parameters{}; - add_endpoint_parameters.endpoint_guid = endpoint_parameters.endpoint_guid; - add_endpoint_parameters.target_compute_system_name = create_vm_parameters.name; - add_endpoint_parameters.nic_mac_address = "00-15-5D-9D-CF-69"; - return add_endpoint_parameters; - }(); - // Create the test network { const auto& [status, status_msg] = hcn.create_network(network_parameters); diff --git a/tests/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/hyperv_api/test_it_hyperv_hcn_api.cpp index 947d2469aa..333300186c 100644 --- a/tests/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -67,7 +67,6 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) endpoint_params.network_guid = network_params.guid; endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; - endpoint_params.endpoint_ipvx_addr = "172.50.224.2"; (void)uut.delete_network(network_params.guid); diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index fe44f67fbb..5312a45846 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -434,11 +434,6 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) }, "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ - ], - "IpConfigurations": [ - { - "IpAddress": "172.50.224.27" - } ] })"""; @@ -480,7 +475,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCNWrapper::create_endpoint(...) > params: Endpoint GUID: (77c27c1e-8204-437d-a7cc-fb4ce1614819) | " - "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68) | Endpoint IPvX Addr.: (172.50.224.27)"); + "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); logger_scope.mock_logger->expect_log(mpl::Level::debug, @@ -496,7 +491,6 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) hyperv::hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [success, error_msg] = uut.create_endpoint(params); ASSERT_TRUE(success); @@ -529,7 +523,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_open_network_failed) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCNWrapper::create_endpoint(...) > params: Endpoint GUID: (77c27c1e-8204-437d-a7cc-fb4ce1614819) | " - "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68) | Endpoint IPvX Addr.: (172.50.224.27)"); + "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); logger_scope.mock_logger->expect_log(mpl::Level::error, @@ -545,7 +539,6 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_open_network_failed) hyperv::hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [status, error_msg] = uut.create_endpoint(params); ASSERT_FALSE(status); @@ -591,11 +584,6 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) }, "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ - ], - "IpConfigurations": [ - { - "IpAddress": "172.50.224.27" - } ] })"""; @@ -634,7 +622,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCNWrapper::create_endpoint(...) > params: Endpoint GUID: (77c27c1e-8204-437d-a7cc-fb4ce1614819) | " - "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68) | Endpoint IPvX Addr.: (172.50.224.27)"); + "Network GUID: (b70c479d-f808-4053-aafa-705bc15b6d68)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_operation(...) > fn: 0x0, result: true"); @@ -649,7 +637,6 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) hyperv::hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [success, error_msg] = uut.create_endpoint(params); ASSERT_FALSE(success); From 08746515e1452281f31d082e2c473a742b608d0f Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 18 Feb 2025 20:27:04 +0300 Subject: [PATCH 02/76] [hyperv-api-vm] initial introduction --- .../hyperv_api/hyperv_api_virtual_machine.cpp | 205 ++++++++++++++++++ .../hyperv_api/hyperv_api_virtual_machine.h | 86 ++++++++ 2 files changed, 291 insertions(+) create mode 100644 src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp create mode 100644 src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp new file mode 100644 index 0000000000..e524b4de7d --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_api_virtual_machine.h" +#include "hcs/hyperv_hcs_compute_system_state.h" +#include + +#include + +namespace multipass::hyperv +{ + +inline auto mac2uuid(std::string mac_addr) +{ + // 00000000-0000-0001-8000-0123456789ab + mac_addr.erase(std::remove(mac_addr.begin(), mac_addr.end(), ':'), mac_addr.end()); + mac_addr.erase(std::remove(mac_addr.begin(), mac_addr.end(), '-'), mac_addr.end()); + constexpr auto format_str = "00000000-0000-0001-8000-{}"; + return fmt::format(format_str, mac_addr); +} + +struct InvalidAPIPointerException : std::runtime_error +{ + InvalidAPIPointerException() : std::runtime_error("") + { + assert(0); + } +}; + +struct CreateComputeSystemException : std::runtime_error +{ + CreateComputeSystemException() : std::runtime_error("") + { + assert(0); + } +}; + +HyperVAPIVirtualMachine::HyperVAPIVirtualMachine( + + unique_hcn_wrapper_t hcn_w, + unique_hcs_wrapper_t hcs_w, + unique_virtdisk_wrapper_t virtdisk_w, + const VirtualMachineDescription& desc, + class VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir) + : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, + description(desc), + hcn(std::move(hcn_w)), + hcs(std::move(hcs_w)), + virtdisk(std::move(virtdisk_w)) +{ + // Verify that the given API wrappers are not null + { + const void* api_ptrs[] = {hcs.get(), hcn.get(), virtdisk.get()}; + if (std::any_of(std::begin(api_ptrs), std::end(api_ptrs), [](const void* ptr) { return nullptr == ptr; })) + { + throw InvalidAPIPointerException{}; + } + } + + // Check if the VM already exist + const auto result = hcs->get_compute_system_state(vm_name); + if (result) + { + // VM already exist. Translate the VM state + // hcs::compute_system_state_from_string(result.status_msg); + } + else + { + // Create the VM from scratch. + const auto ccs_params = [&desc]() { + hcs::CreateComputeSystemParameters ccs_params{}; + ccs_params.name = desc.vm_name; + ccs_params.memory_size_mb = desc.mem_size.in_megabytes(); + ccs_params.processor_count = desc.num_cores; + ccs_params.cloudinit_iso_path = desc.cloud_init_iso.toStdString(); + ccs_params.vhdx_path = desc.image.image_path.toStdString(); + + hcs::AddEndpointParameters default_endpoint_params{}; + default_endpoint_params.nic_mac_address = desc.default_mac_address; + // make the UUID deterministic so we can query the endpoint with a MAC address + // if needed. + default_endpoint_params.endpoint_guid = mac2uuid(desc.default_mac_address); + default_endpoint_params.target_compute_system_name = ccs_params.name; + ccs_params.endpoints.push_back(default_endpoint_params); + + for (const auto& v : desc.extra_interfaces) + { + hcs::AddEndpointParameters endpoint_params{}; + endpoint_params.nic_mac_address = v.mac_address; + endpoint_params.endpoint_guid = mac2uuid(v.mac_address); + endpoint_params.target_compute_system_name = ccs_params.name; + ccs_params.endpoints.push_back(endpoint_params); + } + return ccs_params; + }(); + + if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + { + + throw CreateComputeSystemException{}; + } + else + { + // Create successful + } + } +} + +// HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(const std::string& source_vm_name, +// const multipass::VMSpecs& src_vm_specs, +// const VirtualMachineDescription& desc, +// VMStatusMonitor& monitor, +// const SSHKeyProvider& key_provider, +// const Path& dest_instance_dir) +// { +// } + +void HyperVAPIVirtualMachine::start() +{ + const auto& [status, status_msg] = hcs->start_compute_system(vm_name); +} +void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) +{ + const auto& [status, status_msg] = hcs->shutdown_compute_system(vm_name); +} + +void HyperVAPIVirtualMachine::suspend() +{ + const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); +} +HyperVAPIVirtualMachine::State HyperVAPIVirtualMachine::current_state() +{ + throw std::runtime_error{"Not yet implemented"}; +} +int HyperVAPIVirtualMachine::ssh_port() +{ + constexpr auto kDefaultSSHPort = 22; + return kDefaultSSHPort; +} +std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds timeout) +{ + throw std::runtime_error{"Not yet implemented"}; +} +std::string HyperVAPIVirtualMachine::ssh_username() +{ + return description.ssh_username; +} +std::string HyperVAPIVirtualMachine::management_ipv4() +{ + throw std::runtime_error{"Not yet implemented"}; +} +std::string HyperVAPIVirtualMachine::ipv6() +{ + return {}; +} +void HyperVAPIVirtualMachine::ensure_vm_is_running() +{ + throw std::runtime_error{"Not yet implemented"}; +} +void HyperVAPIVirtualMachine::update_state() +{ + throw std::runtime_error{"Not yet implemented"}; +} +void HyperVAPIVirtualMachine::update_cpus(int num_cores) +{ + throw std::runtime_error{"Not yet implemented"}; +} +void HyperVAPIVirtualMachine::resize_memory(const MemorySize& new_size) +{ + const auto& [status, status_msg] = hcs->resize_memory(vm_name, new_size.in_megabytes()); +} +void HyperVAPIVirtualMachine::resize_disk(const MemorySize& new_size) +{ + throw std::runtime_error{"Not yet implemented"}; +} +void HyperVAPIVirtualMachine::add_network_interface(int index, + const std::string& default_mac_addr, + const NetworkInterface& extra_interface) +{ + + throw std::runtime_error{"Not yet implemented"}; +} +std::unique_ptr HyperVAPIVirtualMachine::make_native_mount_handler(const std::string& target, + const VMMount& mount) +{ + throw std::runtime_error{"Not yet implemented"}; +} + +} // namespace multipass::hyperv \ No newline at end of file diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h new file mode 100644 index 0000000000..40e002d929 --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h @@ -0,0 +1,86 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H +#define MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H + +#include "hcn/hyperv_hcn_api_wrapper.h" +#include "hcs/hyperv_hcs_api_wrapper.h" +#include "virtdisk/virtdisk_api_wrapper.h" +#include +#include + +#include + +namespace multipass::hyperv +{ + +/** + * + */ +struct HyperVAPIVirtualMachine final : public BaseVirtualMachine +{ + using unique_hcn_wrapper_t = std::unique_ptr; + using unique_hcs_wrapper_t = std::unique_ptr; + using unique_virtdisk_wrapper_t = std::unique_ptr; + + HyperVAPIVirtualMachine(unique_hcn_wrapper_t hcn_w, + unique_hcs_wrapper_t hcs_w, + unique_virtdisk_wrapper_t virtdisk_w, + const VirtualMachineDescription& desc, + class VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir); + + HyperVAPIVirtualMachine(unique_hcn_wrapper_t hcn_w, + unique_hcs_wrapper_t hcs_w, + unique_virtdisk_wrapper_t virtdisk_w, + const std::string& source_vm_name, + const multipass::VMSpecs& src_vm_specs, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& dest_instance_dir); + + void start() override; + void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override; + void suspend() override; + State current_state() override; + int ssh_port() override; + std::string ssh_hostname(std::chrono::milliseconds timeout) override; + std::string ssh_username() override; + std::string management_ipv4() override; + std::string ipv6() override; + void ensure_vm_is_running() override; + void update_state() override; + void update_cpus(int num_cores) override; + void resize_memory(const MemorySize& new_size) override; + void resize_disk(const MemorySize& new_size) override; + void add_network_interface(int index, + const std::string& default_mac_addr, + const NetworkInterface& extra_interface) override; + std::unique_ptr make_native_mount_handler(const std::string& target, const VMMount& mount) override; + +private: + const VirtualMachineDescription description; + unique_hcn_wrapper_t hcn{nullptr}; + unique_hcs_wrapper_t hcs{nullptr}; + unique_virtdisk_wrapper_t virtdisk{nullptr}; +}; +} // namespace multipass::hyperv + +#endif From 4d748b8c8cef3fd5ec62ff257ea2193682f35586 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 19 Feb 2025 11:43:59 +0300 Subject: [PATCH 03/76] [hyperv-hcs] better handling of get_compute_system_state --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 33 ++++++------ .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 4 +- .../hcs/hyperv_hcs_compute_system_state.h | 2 +- .../hcs/hyperv_hcs_wrapper_interface.h | 4 +- tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 42 +++++++++++++++ tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 52 ++++++++++++------- 6 files changed, 101 insertions(+), 36 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index ebff4de5d0..8712cdd926 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -175,7 +175,7 @@ OperationResult perform_hcs_operation(const HCSAPITable& api, mpl::error(kLogCategory, "perform_hcs_operation(...) > HcsOpenComputeSystem failed! {}", target_hcs_system_name); - return OperationResult{E_POINTER, L"HcsOpenComputeSystem failed!"}; + return OperationResult{E_INVALIDARG, L"HcsOpenComputeSystem failed!"}; } auto operation = create_operation(api); @@ -523,27 +523,30 @@ OperationResult HCSWrapper::revoke_vm_access(const std::string& compute_system_n // --------------------------------------------------------- -OperationResult HCSWrapper::get_compute_system_state(const std::string& compute_system_name) const +OperationResult HCSWrapper::get_compute_system_state(const std::string& compute_system_name, + ComputeSystemState& state_out) const { mpl::debug(kLogCategory, "get_compute_system_state(...) > name: ({})", compute_system_name); const auto result = perform_hcs_operation(api, api.GetComputeSystemProperties, compute_system_name, nullptr); + if (!result) - { - return {result.code, L"Unknown"}; - } + return result; - const QString qstr{QString::fromStdWString(result.status_msg)}; - const auto doc = QJsonDocument::fromJson(qstr.toUtf8()); - const auto obj = doc.object(); - if (obj.contains("State")) - { - const auto state = obj["State"]; - const auto state_str = state.toString(); - return {result.code, state_str.toStdWString()}; - } + state_out = [json = result.status_msg]() { + QString qstr{QString::fromStdWString(json)}; + const auto doc = QJsonDocument::fromJson(qstr.toUtf8()); + const auto obj = doc.object(); + if (obj.contains("State")) + { + const auto state = obj["State"]; + const auto state_str = state.toString(); + return compute_system_state_from_string(state_str.toStdString()); + } + return ComputeSystemState::stopped; + }(); - return {result.code, L"Unknown"}; + return {result.code, L""}; } } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index d08f7a0753..61d000f455 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -222,11 +222,13 @@ struct HCSWrapper : public HCSWrapperInterface * Retrieve the current state of the compute system. * * @param [in] compute_system_name Target compute system's name + * @param [out] state_out Variable to write the compute system's state * * @return An object that evaluates to true on success, false otherwise. * message() may contain details of failure when result is false. */ - [[nodiscard]] OperationResult get_compute_system_state(const std::string& compute_system_name) const override; + [[nodiscard]] OperationResult get_compute_system_state(const std::string& compute_system_name, + ComputeSystemState& state_out) const override; private: const HCSAPITable api{}; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h index a1dc83bb37..51dd4774cf 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h @@ -51,7 +51,7 @@ enum class ComputeSystemState : std::uint8_t inline std::optional compute_system_state_from_string(std::string str) { std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); }); - // std::unordered_map + static const std::unordered_map translation_map{ {"created", ComputeSystemState::created}, {"running", ComputeSystemState::running}, diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index ea1f7348ef..5fbba001b1 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -19,6 +19,7 @@ #define MULTIPASS_HYPERV_API_HCS_WRAPPER_INTERFACE_H #include +#include #include #include @@ -51,7 +52,8 @@ struct HCSWrapperInterface const std::uint32_t new_size_mib) const = 0; virtual OperationResult update_cpu_count(const std::string& compute_system_name, const std::uint32_t new_core_count) const = 0; - virtual OperationResult get_compute_system_state(const std::string& compute_system_name) const = 0; + virtual OperationResult get_compute_system_state(const std::string& compute_system_name, + ComputeSystemState& state_out) const = 0; virtual ~HCSWrapperInterface() = default; }; } // namespace multipass::hyperv::hcs diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 2a3753ab98..8a38e64692 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -28,6 +28,11 @@ using uut_t = hyperv::hcs::HCSWrapper; struct HyperVHCSAPI_IntegrationTests : public ::testing::Test { + void SetUp() override + { + uut_t uut{}; + (void)uut.terminate_compute_system("test"); + } }; TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) @@ -35,6 +40,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) uut_t uut{}; + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test"; params.memory_size_mb = 1024; @@ -43,6 +50,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) params.vhdx_path = ""; const auto c_result = uut.create_compute_system(params); + ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); + ASSERT_EQ(state, decltype(state)::stopped); ASSERT_TRUE(c_result); ASSERT_TRUE(c_result.status_msg.empty()); @@ -53,6 +62,39 @@ TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) ASSERT_FALSE(d_result.status_msg.empty()); } +TEST_F(HyperVHCSAPI_IntegrationTests, pause_resume_compute_system) +{ + + uut_t uut{}; + + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = "test"; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.cloudinit_iso_path = ""; + params.vhdx_path = ""; + + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + ASSERT_TRUE(uut.create_compute_system(params)); + ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); + ASSERT_EQ(state, decltype(state)::stopped); + ASSERT_TRUE(uut.start_compute_system(params.name)); + ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); + ASSERT_EQ(state, decltype(state)::running); + ASSERT_TRUE(uut.pause_compute_system(params.name)); + ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); + ASSERT_EQ(state, decltype(state)::paused); + ASSERT_TRUE(uut.resume_compute_system(params.name)); + ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); + ASSERT_EQ(state, decltype(state)::running); + const auto d_result = uut.terminate_compute_system(params.name); + ASSERT_TRUE(d_result); + std::wprintf(L"%s\n\n", d_result.status_msg.c_str()); + ASSERT_FALSE(d_result.status_msg.empty()); + + ASSERT_FALSE(uut.get_compute_system_state(params.name, state)); +} + TEST_F(HyperVHCSAPI_IntegrationTests, enumerate_properties) { diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 36effca062..90a0e9926b 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -2597,21 +2597,22 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_wait_for_operation_ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_happy_path) { static wchar_t result_doc[21] = L"{\"State\": \"Running\"}"; - static wchar_t expected_state[8] = L"Running"; generic_operation_happy_path( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...) > name: (test_vm)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state]() { ASSERT_EQ(state, decltype(state)::running); }(); + return result; }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { ASSERT_EQ(mock_compute_system_object, computeSystem); ASSERT_EQ(mock_operation_object, operation); ASSERT_EQ(propertyQuery, nullptr); }, - result_doc, - expected_state); + result_doc); } // --------------------------------------------------------- @@ -2619,33 +2620,41 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_happy_path) TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_no_state) { static wchar_t result_doc[21] = L"{\"Frodo\": \"Baggins\"}"; - static wchar_t expected_state[8] = L"Unknown"; generic_operation_happy_path( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state]() { ASSERT_EQ(state, decltype(state)::stopped); }(); + return result; }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { ASSERT_EQ(mock_compute_system_object, computeSystem); ASSERT_EQ(mock_operation_object, operation); ASSERT_EQ(propertyQuery, nullptr); }, - result_doc, - expected_state); + result_doc); } // --------------------------------------------------------- TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_hcs_open_fail) { - static wchar_t expected_status_msg[] = L"Unknown"; + static wchar_t expected_status_msg[] = L"HcsOpenComputeSystem failed!"; generic_operation_hcs_open_fail( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state, result]() { + ASSERT_EQ(state, decltype(state)::unknown); + ASSERT_EQ(static_cast(result.code), E_INVALIDARG); + }(); + + return result; }, expected_status_msg); } @@ -2654,12 +2663,15 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_hcs_open_fail) TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_create_operation_fail) { - static wchar_t expected_status_msg[] = L"Unknown"; + static wchar_t expected_status_msg[] = L"HcsCreateOperation failed!"; generic_operation_create_operation_fail( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; }, expected_status_msg); } @@ -2668,13 +2680,16 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_create_operation_fail) TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_fail) { - static wchar_t expected_status_msg[] = L"Unknown"; + static wchar_t expected_status_msg[] = L"HCS operation failed!"; generic_operation_fail( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2688,13 +2703,14 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_fail) TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_wait_for_operation_result_fail) { - static wchar_t expected_status_msg[] = L"Unknown"; - generic_operation_wait_for_operation_fail( mock_api_table.GetComputeSystemProperties, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_compute_system_state(...)"); - return wrapper.get_compute_system_state("test_vm"); + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + const auto result = wrapper.get_compute_system_state("test_vm", state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2702,7 +2718,7 @@ TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_wait_for_operation_resul ASSERT_EQ(nullptr, propertyQuery); }, nullptr, - expected_status_msg); + nullptr); } } // namespace multipass::test From 3c7e2043f96134829acee6964af0f157d7f35512 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 24 Feb 2025 12:05:22 +0300 Subject: [PATCH 04/76] [gui/platform] add Hyper-V (API) as a backend option --- src/client/gui/lib/platform/windows.dart | 1 + src/platform/platform_win.cpp | 10 ++++++++-- tests/windows/test_platform_win.cpp | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/client/gui/lib/platform/windows.dart b/src/client/gui/lib/platform/windows.dart index 810b5c0d85..edec364369 100644 --- a/src/client/gui/lib/platform/windows.dart +++ b/src/client/gui/lib/platform/windows.dart @@ -18,6 +18,7 @@ class WindowsPlatform extends MpPlatform { Map get drivers => const { 'hyperv': 'Hyper-V', 'virtualbox': 'VirtualBox', + 'hyperv_api': 'Hyper-V (API)' }; @override diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index de6d732e3f..87950cdd02 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -27,6 +27,7 @@ #include #include "backends/hyperv/hyperv_virtual_machine_factory.h" +#include "backends/hyperv_api/hyperv_api_virtual_machine_factory.h" #include "backends/virtualbox/virtualbox_virtual_machine_factory.h" #include "logger/win_event_logger.h" #include "shared/sshfs_server_process_spec.h" @@ -35,6 +36,7 @@ #include #include + #include #include #include @@ -511,7 +513,7 @@ std::map mp::platform::Platform::get_netw bool mp::platform::Platform::is_backend_supported(const QString& backend) const { - return backend == "hyperv" || backend == "virtualbox"; + return backend == "hyperv" || backend == "virtualbox" || backend == "hyperv_api"; } void mp::platform::Platform::set_server_socket_restrictions(const std::string& /* server_address */, @@ -569,7 +571,7 @@ std::string mp::platform::default_server_address() QString mp::platform::Platform::default_driver() const { - return QStringLiteral("hyperv"); + return QStringLiteral("hyperv_api"); } QString mp::platform::Platform::default_privileged_mounts() const @@ -615,6 +617,10 @@ mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_di return std::make_unique(data_dir); } + else if (driver == "hyperv_api") + { + return std::make_unique(data_dir); + } throw std::runtime_error("Invalid virtualization driver set in the environment"); } diff --git a/tests/windows/test_platform_win.cpp b/tests/windows/test_platform_win.cpp index 12310d5280..5a63613017 100644 --- a/tests/windows/test_platform_win.cpp +++ b/tests/windows/test_platform_win.cpp @@ -143,7 +143,7 @@ TEST(PlatformWin, no_extra_daemon_settings) TEST(PlatformWin, test_default_driver) { - EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "virtualbox")); + EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "hyperv_api", "virtualbox")); } TEST(PlatformWin, test_default_privileged_mounts) From 237ed9169d0bfdf5202bad1d1eee1ddb3d9c0c50 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 24 Feb 2025 12:07:30 +0300 Subject: [PATCH 05/76] [.gitignore] add network-cache to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 84b41e0feb..8b110e9ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ packaging/windows/custom-actions/x64/* # clangd cache path .cache/ +**/network-cache From 020dfbc1eb47eb33c1d4101bca04f1bc4fc6022f Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 24 Feb 2025 13:39:52 +0300 Subject: [PATCH 06/76] [hyperv-hcs] handle superfluous comma better in scsi and network interface handling --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 60 +++++++++---------- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 25 ++++---- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 8712cdd926..f8cb967ed4 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -224,58 +224,56 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam // available drives. const auto scsi_devices = [¶ms]() { constexpr auto scsi_device_template = LR"( - "{0}": {{ - "Attachments": {{ - "0": {{ - "Type": "{1}", - "Path": "{2}", - "ReadOnly": {3} - }} + "{0}": {{ + "Attachments": {{ + "0": {{ + "Type": "{1}", + "Path": "{2}", + "ReadOnly": {3} }} - }}, - )"; - std::wstring result = {}; + }} + }})"; + std::vector scsi_nodes{}; + if (!params.cloudinit_iso_path.empty()) { - result += fmt::format(scsi_device_template, - L"cloud-init iso file", - L"Iso", - string_to_wstring(params.cloudinit_iso_path), - true); + scsi_nodes.push_back(fmt::format(scsi_device_template, + L"cloud-init iso file", + L"Iso", + string_to_wstring(params.cloudinit_iso_path), + true)); } if (!params.vhdx_path.empty()) { - result += fmt::format(scsi_device_template, - L"Primary disk", - L"VirtualDisk", - string_to_wstring(params.vhdx_path), - false); + scsi_nodes.push_back(fmt::format(scsi_device_template, + L"Primary disk", + L"VirtualDisk", + string_to_wstring(params.vhdx_path), + false)); } - return result; + + return fmt::format(L"{}", fmt::join(scsi_nodes, L", ")); }(); const auto network_adapters = [&]() { - std::wstring result = {}; + std::vector network_adapters = {}; constexpr auto network_adapter_template = LR"( "{0}": {{ "EndpointId" : "{0}", "MacAddress": "{1}" - }},)"; + }})"; for (const auto& endpoint : params.endpoints) { - result += fmt::format(network_adapter_template, - string_to_wstring(endpoint.endpoint_guid), - string_to_wstring(endpoint.nic_mac_address)); + network_adapters.push_back(fmt::format(network_adapter_template, + string_to_wstring(endpoint.endpoint_guid), + string_to_wstring(endpoint.nic_mac_address))); } - // Remove the last comma. - if (!result.empty()) - result.pop_back(); - - return result; + return fmt::format(L"{}", fmt::join(network_adapters, L", ")); + ; }(); // Ideally, we should codegen from the schema diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 90a0e9926b..3150509d39 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -236,8 +236,9 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) "ReadOnly": false } } - }, - } + } + }, + "NetworkAdapters": {} } } })"; @@ -393,8 +394,9 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) "ReadOnly": false } } - }, - } + } + }, + "NetworkAdapters": {} } } })"; @@ -550,8 +552,9 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) "ReadOnly": true } } - }, - } + } + }, + "NetworkAdapters": {} } } })"; @@ -909,8 +912,9 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) "ReadOnly": false } } - }, - } + } + }, + "NetworkAdapters": {} } } })"; @@ -1052,8 +1056,9 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) "ReadOnly": false } } - }, - } + } + }, + "NetworkAdapters": {} } } })"; From 2b36ce895077c00b863317e40f1d1d0fd280415d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 27 Feb 2025 09:58:39 +0300 Subject: [PATCH 07/76] [hyperv-api-factory] introduce the factory type - [vm] take network guid as a constructor argument - [vm] add ability to resolve hostname to retrieve IP address - [vm-start] distinguish between paused and stopped state and act accordingly - [vm-stop] take ShutdownPolicy into consideration - [vm-ssh-hostname] return the automatic ".mshome.net" hostname - [vm-ipv4-ipv6] return the resolved ip's for .mshome.net hostname Signed-off-by: Mustafa Kemal Gilor --- .../backends/hyperv_api/CMakeLists.txt | 2 + .../hyperv_api/hyperv_api_virtual_machine.cpp | 377 +++++++++++++++--- .../hyperv_api/hyperv_api_virtual_machine.h | 27 +- .../hyperv_api_virtual_machine_factory.cpp | 162 ++++++++ .../hyperv_api_virtual_machine_factory.h | 66 +++ 5 files changed, 561 insertions(+), 73 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp create mode 100644 src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index ccfdf570df..72d447357a 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -45,6 +45,8 @@ if(WIN32) hcn/hyperv_hcn_api_wrapper.cpp hcs/hyperv_hcs_api_wrapper.cpp virtdisk/virtdisk_api_wrapper.cpp + hyperv_api_virtual_machine.cpp + hyperv_api_virtual_machine_factory.cpp ) target_link_libraries(hyperv_api_backend PRIVATE diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp index e524b4de7d..46203934bd 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -16,52 +16,161 @@ */ #include "hyperv_api_virtual_machine.h" +#include "hcn/hyperv_hcn_create_endpoint_params.h" #include "hcs/hyperv_hcs_compute_system_state.h" #include +#include +#include + +#include #include -namespace multipass::hyperv +#include + +namespace { +/** + * Category for the log messages. + */ +constexpr auto kLogCategory = "HyperV-Virtual-Machine"; + +namespace mpl = multipass::logging; +using lvl = mpl::Level; + inline auto mac2uuid(std::string mac_addr) { // 00000000-0000-0001-8000-0123456789ab mac_addr.erase(std::remove(mac_addr.begin(), mac_addr.end(), ':'), mac_addr.end()); mac_addr.erase(std::remove(mac_addr.begin(), mac_addr.end(), '-'), mac_addr.end()); - constexpr auto format_str = "00000000-0000-0001-8000-{}"; + constexpr auto format_str = "db4bdbf0-dc14-407f-9780-{}"; return fmt::format(format_str, mac_addr); } +// db4bdbf0-dc14-407f-9780-f528c59b0c58 +inline auto replace_colon_with_dash(std::string& addr) +{ + if (addr.empty()) + return; + std::replace(addr.begin(), addr.end(), ':', '-'); +} -struct InvalidAPIPointerException : std::runtime_error +auto resolve_ip_addresses(const std::string& hostname) { - InvalidAPIPointerException() : std::runtime_error("") + static auto wsa_context = [] { + WSADATA wsaData{}; + if (const auto r = WSAStartup(MAKEWORD(2, 2), &wsaData); r != 0) + { + // LOG HERE + mpl::log(lvl::error, + kLogCategory, + fmt::format("resolve_and_memoize_ip_addresses() > WSAStartup failed with {}!", r)); + } + + struct auto_destroy + { + ~auto_destroy() + { + WSACleanup(); + } + } a; + return a; + }(); + + // Wrap the raw addrinfo pointer so it's always destroyed properly. + const auto& [result, addr_info] = [&]() { + struct addrinfo* result = {nullptr}; + struct addrinfo hints{}; + const auto r = getaddrinfo(hostname.c_str(), nullptr, nullptr, &result); + return std::make_pair(r, std::unique_ptr{result, freeaddrinfo}); + }(); + + std::vector ipv4{}, ipv6{}; + if (result == 0) { - assert(0); + assert(addr_info.get()); + for (auto ptr = addr_info.get(); ptr != nullptr; ptr = addr_info->ai_next) + { + switch (ptr->ai_family) + { + case AF_INET: + { + 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; + case AF_INET6: + { + const auto sockaddr_ipv6 = reinterpret_cast(ptr->ai_addr); + char addr[INET6_ADDRSTRLEN] = {}; + inet_ntop(AF_INET6, &(sockaddr_ipv6->sa_data), addr, sizeof(addr)); + ipv6.push_back(addr); + } + break; + default: + continue; + } + } } -}; + return std::make_pair(ipv4, ipv6); +} +} // namespace + +namespace multipass::hyperv +{ -struct CreateComputeSystemException : std::runtime_error +struct ExceptionFormatter : std::runtime_error { - CreateComputeSystemException() : std::runtime_error("") + template + ExceptionFormatter(fmt::format_string fmt, Args&&... args) + : std::runtime_error{fmt::format(fmt, std::forward(args)...)} + { + } + ExceptionFormatter() : std::runtime_error("") { - assert(0); } }; -HyperVAPIVirtualMachine::HyperVAPIVirtualMachine( +struct InvalidAPIPointerException : ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +struct CreateComputeSystemException : ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +struct ComputeSystemStateException : ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +struct CreateEndpointException : ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +struct GrantVMAccessException : ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; - unique_hcn_wrapper_t hcn_w, - unique_hcs_wrapper_t hcs_w, - unique_virtdisk_wrapper_t virtdisk_w, - const VirtualMachineDescription& desc, - class VMStatusMonitor& monitor, - const SSHKeyProvider& key_provider, - const Path& instance_dir) +HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, + unique_hcn_wrapper_t hcn_w, + unique_virtdisk_wrapper_t virtdisk_w, + const std::string& network_guid, + const VirtualMachineDescription& desc, + class VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir) : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, description(desc), - hcn(std::move(hcn_w)), hcs(std::move(hcs_w)), + hcn(std::move(hcn_w)), + monitor(monitor), virtdisk(std::move(virtdisk_w)) { // Verify that the given API wrappers are not null @@ -73,53 +182,98 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine( } } - // Check if the VM already exist - const auto result = hcs->get_compute_system_state(vm_name); - if (result) - { - // VM already exist. Translate the VM state - // hcs::compute_system_state_from_string(result.status_msg); - } - else + hcs::ComputeSystemState cs_state{hcs::ComputeSystemState::unknown}; + { - // Create the VM from scratch. - const auto ccs_params = [&desc]() { - hcs::CreateComputeSystemParameters ccs_params{}; - ccs_params.name = desc.vm_name; - ccs_params.memory_size_mb = desc.mem_size.in_megabytes(); - ccs_params.processor_count = desc.num_cores; - ccs_params.cloudinit_iso_path = desc.cloud_init_iso.toStdString(); - ccs_params.vhdx_path = desc.image.image_path.toStdString(); - - hcs::AddEndpointParameters default_endpoint_params{}; - default_endpoint_params.nic_mac_address = desc.default_mac_address; - // make the UUID deterministic so we can query the endpoint with a MAC address - // if needed. - default_endpoint_params.endpoint_guid = mac2uuid(desc.default_mac_address); - default_endpoint_params.target_compute_system_name = ccs_params.name; - ccs_params.endpoints.push_back(default_endpoint_params); - - for (const auto& v : desc.extra_interfaces) + // Check if the VM already exist + const auto result = hcs->get_compute_system_state(vm_name, cs_state); + + if (E_INVALIDARG == static_cast(result.code)) + { + + const auto endpoint_params = [&desc, &network_guid]() { + std::vector endpoint_params{}; + endpoint_params.emplace_back( + hcn::CreateEndpointParameters{network_guid, mac2uuid(desc.default_mac_address)}); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); + // } + return endpoint_params; + }(); + + for (const auto& endpoint : endpoint_params) { - hcs::AddEndpointParameters endpoint_params{}; - endpoint_params.nic_mac_address = v.mac_address; - endpoint_params.endpoint_guid = mac2uuid(v.mac_address); - endpoint_params.target_compute_system_name = ccs_params.name; - ccs_params.endpoints.push_back(endpoint_params); + // There might be remnants from an old VM, remove the endpoint if exist before + // creating it again. + if (hcn->delete_endpoint(endpoint.endpoint_guid)) + { + // TODO: log + } + if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) + { + throw CreateEndpointException{}; + } } - return ccs_params; - }(); - if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) - { + // E_INVALIDARG means there's no such VM. + // Create the VM from scratch. + const auto ccs_params = [&desc, &endpoint_params]() { + hcs::CreateComputeSystemParameters ccs_params{}; + ccs_params.name = desc.vm_name; + ccs_params.memory_size_mb = desc.mem_size.in_megabytes(); + ccs_params.processor_count = desc.num_cores; + ccs_params.cloudinit_iso_path = desc.cloud_init_iso.toStdString(); + ccs_params.vhdx_path = desc.image.image_path.toStdString(); - throw CreateComputeSystemException{}; - } - else - { - // Create successful + hcs::AddEndpointParameters default_endpoint_params{}; + default_endpoint_params.nic_mac_address = desc.default_mac_address; + // Hyper-V API does not like colons. Ensure that the MAC is separated + // with dash instead of colon. + replace_colon_with_dash(default_endpoint_params.nic_mac_address); + // make the UUID deterministic so we can query the endpoint with a MAC address + // if needed. + default_endpoint_params.endpoint_guid = mac2uuid(desc.default_mac_address); + default_endpoint_params.target_compute_system_name = ccs_params.name; + ccs_params.endpoints.push_back(default_endpoint_params); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // hcs::AddEndpointParameters endpoint_params{}; + // endpoint_params.nic_mac_address = v.mac_address; + // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); + // endpoint_params.target_compute_system_name = ccs_params.name; + // ccs_params.endpoints.push_back(endpoint_params); + // } + return ccs_params; + }(); + + if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + { + fmt::print(L"{}", create_result.status_msg); + throw CreateComputeSystemException{}; + } + + // Grant access to the VHDX and the cloud-init ISO files. + const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; + + for (const auto& path : grant_paths) + { + if (!hcs->grant_vm_access(ccs_params.name, path)) + { + throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", + ccs_params.name, + path}; + } + } } } + + // Reflect compute system's state + update_state(); } // HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(const std::string& source_vm_name, @@ -131,22 +285,90 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine( // { // } +void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) +{ + const auto prev_state = state; + switch (compute_system_state) + { + case hcs::ComputeSystemState::created: + state = State::off; + break; + case hcs::ComputeSystemState::paused: + state = State::suspended; + break; + case hcs::ComputeSystemState::running: + state = State::running; + break; + case hcs::ComputeSystemState::saved_as_template: + case hcs::ComputeSystemState::stopped: + state = State::stopped; + break; + case hcs::ComputeSystemState::unknown: + state = State::unknown; + break; + } + + if (state != prev_state) + { + mpl::log(lvl::info, + kLogCategory, + fmt::format("set_state() > VM {} state changed from {} to {}", + vm_name, + fmt::underlying(prev_state), + fmt::underlying(state))); + } +} + void HyperVAPIVirtualMachine::start() { - const auto& [status, status_msg] = hcs->start_compute_system(vm_name); + // Fetch the latest state value. + update_state(); + + // Resume and start are the same thing in Multipass terms + // Try to determine whether we need to resume or start here. + const auto& [status, status_msg] = [&] { + if (state == VirtualMachine::State::suspended) + { + return hcs->resume_compute_system(vm_name); + } + return hcs->start_compute_system(vm_name); + }(); + + state = VirtualMachine::State::starting; + monitor.persist_state_for(vm_name, state); + update_state(); } void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { - const auto& [status, status_msg] = hcs->shutdown_compute_system(vm_name); + + switch (shutdown_policy) + { + case ShutdownPolicy::Powerdown: + // We have to rely on SSH for now since we don't have the means to + // run a guest action from host natively. + // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall + // or other more "direct" means to trigger the shutdown. + ssh_exec("sudo shutdown -h now"); + break; + case ShutdownPolicy::Halt: + case ShutdownPolicy::Poweroff: + // These are non-graceful variants. Just terminate the system immediately. + hcs->terminate_compute_system(vm_name); + break; + } + drop_ssh_session(); + state = State::off; } void HyperVAPIVirtualMachine::suspend() { const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); + update_state(); } + HyperVAPIVirtualMachine::State HyperVAPIVirtualMachine::current_state() { - throw std::runtime_error{"Not yet implemented"}; + return state; } int HyperVAPIVirtualMachine::ssh_port() { @@ -155,27 +377,50 @@ int HyperVAPIVirtualMachine::ssh_port() } std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds timeout) { - throw std::runtime_error{"Not yet implemented"}; + (void)timeout; + constexpr auto hostname_pattern = "{}.mshome.net"; + return fmt::format(hostname_pattern, vm_name); } std::string HyperVAPIVirtualMachine::ssh_username() { return description.ssh_username; } + std::string HyperVAPIVirtualMachine::management_ipv4() { - throw std::runtime_error{"Not yet implemented"}; + const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); + if (ipv4.empty()) + { + return {}; + } + + const auto result = *ipv4.begin(); + + mpl::log(lvl::info, kLogCategory, fmt::format("management_ipv4() > IP address is `{}`", result)); + // Prefer the first one + return result; } std::string HyperVAPIVirtualMachine::ipv6() { - return {}; + const auto& [_, ipv6] = resolve_ip_addresses(ssh_hostname({}).c_str()); + if (ipv6.empty()) + { + return {}; + } + // Prefer the first one + return *ipv6.begin(); } void HyperVAPIVirtualMachine::ensure_vm_is_running() { - throw std::runtime_error{"Not yet implemented"}; + auto is_vm_running = [this] { return state != State::off; }; + multipass::backend::ensure_vm_is_running_for(this, is_vm_running, "Instance shutdown during start"); } void HyperVAPIVirtualMachine::update_state() { - throw std::runtime_error{"Not yet implemented"}; + hcs::ComputeSystemState compute_system_state{hcs::ComputeSystemState::unknown}; + const auto result = hcs->get_compute_system_state(vm_name, compute_system_state); + set_state(compute_system_state); + monitor.persist_state_for(vm_name, state); } void HyperVAPIVirtualMachine::update_cpus(int num_cores) { diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h index 40e002d929..69e128bf82 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h @@ -26,11 +26,16 @@ #include +namespace multipass +{ +class VMStatusMonitor; +} + namespace multipass::hyperv { /** - * + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. */ struct HyperVAPIVirtualMachine final : public BaseVirtualMachine { @@ -38,16 +43,17 @@ struct HyperVAPIVirtualMachine final : public BaseVirtualMachine using unique_hcs_wrapper_t = std::unique_ptr; using unique_virtdisk_wrapper_t = std::unique_ptr; - HyperVAPIVirtualMachine(unique_hcn_wrapper_t hcn_w, - unique_hcs_wrapper_t hcs_w, + HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, + unique_hcn_wrapper_t hcn_w, unique_virtdisk_wrapper_t virtdisk_w, + const std::string& network_guid, const VirtualMachineDescription& desc, - class VMStatusMonitor& monitor, + VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, const Path& instance_dir); - HyperVAPIVirtualMachine(unique_hcn_wrapper_t hcn_w, - unique_hcs_wrapper_t hcs_w, + HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, + unique_hcn_wrapper_t hcn_w, unique_virtdisk_wrapper_t virtdisk_w, const std::string& source_vm_name, const multipass::VMSpecs& src_vm_specs, @@ -75,11 +81,18 @@ struct HyperVAPIVirtualMachine final : public BaseVirtualMachine const NetworkInterface& extra_interface) override; std::unique_ptr make_native_mount_handler(const std::string& target, const VMMount& mount) override; +protected: + void require_snapshots_support() const override{} + private: const VirtualMachineDescription description; - unique_hcn_wrapper_t hcn{nullptr}; unique_hcs_wrapper_t hcs{nullptr}; + unique_hcn_wrapper_t hcn{nullptr}; unique_virtdisk_wrapper_t virtdisk{nullptr}; + + VMStatusMonitor& monitor; + + void set_state(hcs::ComputeSystemState state); }; } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp new file mode 100644 index 0000000000..ddb0a655ee --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_api_virtual_machine_factory.h" +#include "hyperv_api_virtual_machine.h" +#include +#include + +#include + +#include // for the std::filesystem::path formatter + +namespace multipass::hyperv +{ + +/** + * Category for the log messages. + */ +constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; + +struct ExceptionFormatter : std::runtime_error +{ + template + ExceptionFormatter(fmt::format_string fmt, Args&&... args) + : std::runtime_error{fmt::format(fmt, std::forward(args)...)} + { + } + + ExceptionFormatter() : std::runtime_error("") + { + } +}; + +struct ImageConversionException : public ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +struct ImageResizeException : public ExceptionFormatter +{ + using ExceptionFormatter::ExceptionFormatter; +}; + +HyperVAPIVirtualMachineFactory::HyperVAPIVirtualMachineFactory(const Path& data_dir) + : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) +{ +} + +VirtualMachine::UPtr HyperVAPIVirtualMachineFactory::create_virtual_machine(const VirtualMachineDescription& desc, + const SSHKeyProvider& key_provider, + VMStatusMonitor& monitor) +{ + auto hcs = std::make_unique(); + auto hcn = std::make_unique(); + auto virtdisk = std::make_unique(); + + static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; + + return std::make_unique(std::move(hcs), + std::move(hcn), + std::move(virtdisk), + kDefaultHyperVSwitchGUID, + desc, + monitor, + key_provider, + get_instance_directory(desc.vm_name)); +} + +void HyperVAPIVirtualMachineFactory::remove_resources_for_impl(const std::string& name) +{ + throw std::runtime_error{"Not implemented yet."}; +} + +VMImage HyperVAPIVirtualMachineFactory::prepare_source_image(const VMImage& source_image) +{ + const std::filesystem::path source_file{source_image.image_path.toStdString()}; + + if (!std::filesystem::exists(source_file)) + { + throw ImageConversionException{"Image {} does not exist", source_file}; + } + + const std::filesystem::path target_file = [source_file]() { + auto target_file = source_file; + target_file.replace_extension(".vhdx"); + return target_file; + }(); + + const QStringList qemu_img_args{"convert", + "-o", + "subformat=dynamic", + "-O", + "vhdx", + QString::fromStdString(source_file.string()), + QString::fromStdString(target_file.string())}; + + QProcess qemu_img_process{}; + qemu_img_process.setProgram("qemu-img.exe"); + qemu_img_process.setArguments(qemu_img_args); + qemu_img_process.start(); + + if (!qemu_img_process.waitForFinished(multipass::image_resize_timeout)) + { + throw ImageConversionException{"Conversion of image {} to VHDX timed out", source_file}; + } + + if (qemu_img_process.exitCode() != 0) + { + throw ImageConversionException{"Conversion of image {} to VHDX failed with following error: {}", + source_file, + qemu_img_process.readAllStandardError().toStdString()}; + } + + if (!std::filesystem::exists(target_file)) + { + throw ImageConversionException{"Converted VHDX `{}` does not exist!", target_file}; + } + + VMImage result{source_image}; + result.image_path = QString::fromStdString(target_file.string()); + return result; +} + +void HyperVAPIVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, + const VirtualMachineDescription& desc) +{ + // FIXME: + virtdisk::VirtDiskWrapper wrap{}; + + // Resize the instance image to the desired size + const auto& [status, status_msg] = + wrap.resize_virtual_disk(instance_image.image_path.toStdString(), desc.disk_space.in_bytes()); + if (!status) + { + throw ImageResizeException{"Failed to resize VHDX file `{}`, virtdisk API error code `{}`", + instance_image.image_path.toStdString(), + status}; + } +} + +std::string HyperVAPIVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) +{ + (void)intf; + // No-op. The implementation uses the default Hyper-V switch. + return {}; +} + +} // namespace multipass::hyperv \ No newline at end of file diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h new file mode 100644 index 0000000000..685286b470 --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h @@ -0,0 +1,66 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_FACTORY_H +#define MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_FACTORY_H + +#include + +namespace multipass::hyperv +{ + +/** + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. + */ +struct HyperVAPIVirtualMachineFactory final : public BaseVirtualMachineFactory +{ + explicit HyperVAPIVirtualMachineFactory(const Path& data_dir); + + VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, + const SSHKeyProvider& key_provider, + VMStatusMonitor& monitor) override; + + VMImage prepare_source_image(const VMImage& source_image) override; + void prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) override; + void hypervisor_health_check() override + { + } + + QString get_backend_version_string() const override + { + return "hyperv_api"; + }; + + std::vector networks() const override + { + return {}; + } + + void require_snapshots_support() const override + { + } + void require_clone_support() const override + { + } + +protected: + std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; + void remove_resources_for_impl(const std::string& name) override; +}; +} // namespace multipass::hyperv + +#endif From 2eced8d10a7cfc26b437cae827309e37b7c7cb6a Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 4 Mar 2025 13:32:21 +0300 Subject: [PATCH 08/76] [hyperv-api-vm] simplify state management resolve_ip_addresses: simplify the wsa_init_wrapper --- .../hyperv_api/hyperv_api_virtual_machine.cpp | 82 ++++++++++++------- .../hyperv_api/hyperv_api_virtual_machine.h | 1 + 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp index 46203934bd..43b01102cb 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -58,23 +58,33 @@ inline auto replace_colon_with_dash(std::string& addr) auto resolve_ip_addresses(const std::string& hostname) { static auto wsa_context = [] { - WSADATA wsaData{}; - if (const auto r = WSAStartup(MAKEWORD(2, 2), &wsaData); r != 0) + struct wsa_init_wrapper { - // LOG HERE - mpl::log(lvl::error, - kLogCategory, - fmt::format("resolve_and_memoize_ip_addresses() > WSAStartup failed with {}!", r)); - } - - struct auto_destroy - { - ~auto_destroy() + wsa_init_wrapper() : wsa_data{}, wsa_init_success(WSAStartup(MAKEWORD(2, 2), &wsa_data)) { - WSACleanup(); + if (!wsa_init_success) + { + mpl::log(lvl::error, kLogCategory, "resolve_and_memoize_ip_addresses() > WSAStartup failed!"); + } } - } a; - return a; + ~wsa_init_wrapper() + { + /** + * https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsacleanup + * There must be a call to WSACleanup for each successful call to WSAStartup. + * Only the final WSACleanup function call performs the actual cleanup. + * The preceding calls simply decrement an internal reference count in the WS2_32.DLL. + */ + if (wsa_init_success) + { + WSACleanup(); + } + } + WSADATA wsa_data{}; + const bool wsa_init_success{false}; + }; + + return wsa_init_wrapper{}; }(); // Wrap the raw addrinfo pointer so it's always destroyed properly. @@ -271,8 +281,8 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, } } } - // Reflect compute system's state + set_state(fetch_state_from_api()); update_state(); } @@ -321,21 +331,24 @@ void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_s void HyperVAPIVirtualMachine::start() { - // Fetch the latest state value. + state = VirtualMachine::State::starting; update_state(); - // Resume and start are the same thing in Multipass terms // Try to determine whether we need to resume or start here. const auto& [status, status_msg] = [&] { - if (state == VirtualMachine::State::suspended) + // Fetch the latest state value. + if (fetch_state_from_api() == hcs::ComputeSystemState::paused) { return hcs->resume_compute_system(vm_name); } return hcs->start_compute_system(vm_name); }(); - state = VirtualMachine::State::starting; - monitor.persist_state_for(vm_name, state); + // TODO: Check status message here + + // Maybe wait until SSH is up with timeout + std::this_thread::sleep_for(std::chrono::seconds{60}); + set_state(fetch_state_from_api()); update_state(); } void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) @@ -346,23 +359,28 @@ void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) case ShutdownPolicy::Powerdown: // We have to rely on SSH for now since we don't have the means to // run a guest action from host natively. - // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall - // or other more "direct" means to trigger the shutdown. + // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall, + // some way to signal "vmwp.exe" for a graceful shutdown, sysrq via console + // or other "direct" means to trigger the shutdown. ssh_exec("sudo shutdown -h now"); + drop_ssh_session(); break; case ShutdownPolicy::Halt: case ShutdownPolicy::Poweroff: + drop_ssh_session(); // These are non-graceful variants. Just terminate the system immediately. hcs->terminate_compute_system(vm_name); break; } - drop_ssh_session(); + state = State::off; + update_state(); } void HyperVAPIVirtualMachine::suspend() { const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); + set_state(fetch_state_from_api()); update_state(); } @@ -375,11 +393,9 @@ int HyperVAPIVirtualMachine::ssh_port() constexpr auto kDefaultSSHPort = 22; return kDefaultSSHPort; } -std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds timeout) +std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) { - (void)timeout; - constexpr auto hostname_pattern = "{}.mshome.net"; - return fmt::format(hostname_pattern, vm_name); + return fmt::format("{}.mshome.net", vm_name); } std::string HyperVAPIVirtualMachine::ssh_username() { @@ -391,6 +407,7 @@ std::string HyperVAPIVirtualMachine::management_ipv4() const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv4.empty()) { + // TODO: Log return {}; } @@ -405,6 +422,7 @@ std::string HyperVAPIVirtualMachine::ipv6() const auto& [_, ipv6] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv6.empty()) { + // TODO: Log return {}; } // Prefer the first one @@ -416,12 +434,17 @@ void HyperVAPIVirtualMachine::ensure_vm_is_running() multipass::backend::ensure_vm_is_running_for(this, is_vm_running, "Instance shutdown during start"); } void HyperVAPIVirtualMachine::update_state() +{ + monitor.persist_state_for(vm_name, state); +} + +hcs::ComputeSystemState HyperVAPIVirtualMachine::fetch_state_from_api() { hcs::ComputeSystemState compute_system_state{hcs::ComputeSystemState::unknown}; const auto result = hcs->get_compute_system_state(vm_name, compute_system_state); - set_state(compute_system_state); - monitor.persist_state_for(vm_name, state); + return compute_system_state; } + void HyperVAPIVirtualMachine::update_cpus(int num_cores) { throw std::runtime_error{"Not yet implemented"}; @@ -438,7 +461,6 @@ void HyperVAPIVirtualMachine::add_network_interface(int index, const std::string& default_mac_addr, const NetworkInterface& extra_interface) { - throw std::runtime_error{"Not yet implemented"}; } std::unique_ptr HyperVAPIVirtualMachine::make_native_mount_handler(const std::string& target, diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h index 69e128bf82..2423aa5161 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h @@ -92,6 +92,7 @@ struct HyperVAPIVirtualMachine final : public BaseVirtualMachine VMStatusMonitor& monitor; + hcs::ComputeSystemState fetch_state_from_api(); void set_state(hcs::ComputeSystemState state); }; } // namespace multipass::hyperv From bb9dda60daf16bb52a4f19df48483b90dbfec981 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 7 Mar 2025 13:20:04 +0300 Subject: [PATCH 09/76] [hyperv-hcs-api] adapt to std::optional return --- .../backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index f8cb967ed4..fa9754e4fd 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -539,7 +539,12 @@ OperationResult HCSWrapper::get_compute_system_state(const std::string& compute_ { const auto state = obj["State"]; const auto state_str = state.toString(); - return compute_system_state_from_string(state_str.toStdString()); + const auto ccs = compute_system_state_from_string(state_str.toStdString()); + if (ccs) + { + return ccs.value(); + } + return ComputeSystemState::unknown; } return ComputeSystemState::stopped; }(); From 757a525b5274e4a9ad30a98c9e0b34fbf231d7ff Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 7 Mar 2025 14:07:22 +0300 Subject: [PATCH 10/76] [virtual-machine] add fmt formatter for state enum --- include/multipass/virtual_machine.h | 56 +++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 57e5b302b1..16d70cba75 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -34,6 +34,8 @@ #include #include +#include + namespace multipass { class MemorySize; @@ -78,7 +80,7 @@ class VirtualMachine : private DisabledCopyMove virtual std::string ssh_hostname() { return ssh_hostname(std::chrono::minutes(2)); - }; + } virtual std::string ssh_hostname(std::chrono::milliseconds timeout) = 0; virtual std::string ssh_username() = 0; virtual std::string management_ipv4() = 0; @@ -136,7 +138,7 @@ class VirtualMachine : private DisabledCopyMove VirtualMachine(VirtualMachine::State state, const std::string& vm_name, const Path& instance_dir) : state{state}, vm_name{vm_name}, instance_dir{QDir{instance_dir}} {}; VirtualMachine(const std::string& vm_name, const Path& instance_dir) - : VirtualMachine(State::off, vm_name, instance_dir){}; + : VirtualMachine(State::off, vm_name, instance_dir) {}; }; } // namespace multipass @@ -145,4 +147,54 @@ inline QDir multipass::VirtualMachine::instance_directory() const return instance_dir; // TODO this should probably only be known at the level of the base VM } +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(multipass::VirtualMachine::State state, FormatContext& ctx) const + { + std::string_view v = "(undefined)"; + switch (state) + { + case multipass::VirtualMachine::State::off: + v = "off"; + break; + case multipass::VirtualMachine::State::stopped: + v = "stopped"; + break; + case multipass::VirtualMachine::State::starting: + v = "starting"; + break; + case multipass::VirtualMachine::State::restarting: + v = "restarting"; + break; + case multipass::VirtualMachine::State::running: + v = "running"; + break; + case multipass::VirtualMachine::State::delayed_shutdown: + v = "delayed_shutdown"; + break; + case multipass::VirtualMachine::State::suspending: + v = "suspending"; + break; + case multipass::VirtualMachine::State::suspended: + v = "suspended"; + break; + case multipass::VirtualMachine::State::unknown: + v = "unknown"; + break; + } + + return format_to(ctx.out(), "{}", v); + } +}; + #endif // MULTIPASS_VIRTUAL_MACHINE_H From d5523f483c232980318fcaf685bbde7eb802f60d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 7 Mar 2025 15:11:51 +0300 Subject: [PATCH 11/76] [hyperv-api-vm] update log levels, remove fmt::format from log calls --- .../hcs/hyperv_hcs_compute_system_state.h | 2 ++ .../hyperv_api/hyperv_api_virtual_machine.cpp | 25 ++++++++++--------- .../hyperv_api_virtual_machine_factory.cpp | 7 +++--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h index 51dd4774cf..96f205dc72 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h @@ -24,6 +24,8 @@ #include #include +#include + namespace multipass::hyperv::hcs { diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp index 43b01102cb..c756bb7cde 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -322,15 +322,16 @@ void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_s { mpl::log(lvl::info, kLogCategory, - fmt::format("set_state() > VM {} state changed from {} to {}", - vm_name, - fmt::underlying(prev_state), - fmt::underlying(state))); + "set_state() > VM {} state changed from {} to {}", + vm_name, + prev_state, + state); } } void HyperVAPIVirtualMachine::start() { + mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, state {}", vm_name, state); state = VirtualMachine::State::starting; update_state(); // Resume and start are the same thing in Multipass terms @@ -339,17 +340,17 @@ void HyperVAPIVirtualMachine::start() // Fetch the latest state value. if (fetch_state_from_api() == hcs::ComputeSystemState::paused) { + mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in paused state, resuming", vm_name); return hcs->resume_compute_system(vm_name); } + mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); return hcs->start_compute_system(vm_name); }(); - // TODO: Check status message here - - // Maybe wait until SSH is up with timeout - std::this_thread::sleep_for(std::chrono::seconds{60}); - set_state(fetch_state_from_api()); - update_state(); + // // Maybe wait until SSH is up with timeout + // wait_until_ssh_up(std::chrono::seconds{240}); + // set_state(fetch_state_from_api()); + // update_state(); } void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { @@ -413,7 +414,7 @@ std::string HyperVAPIVirtualMachine::management_ipv4() const auto result = *ipv4.begin(); - mpl::log(lvl::info, kLogCategory, fmt::format("management_ipv4() > IP address is `{}`", result)); + mpl::log(lvl::info, kLogCategory, "management_ipv4() > IP address is `{}`", result); // Prefer the first one return result; } @@ -469,4 +470,4 @@ std::unique_ptr HyperVAPIVirtualMachine::make_native_mount_handler throw std::runtime_error{"Not yet implemented"}; } -} // namespace multipass::hyperv \ No newline at end of file +} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp index ddb0a655ee..16a4f1aae6 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp @@ -30,7 +30,8 @@ namespace multipass::hyperv /** * Category for the log messages. */ -constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; +static constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; +static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; struct ExceptionFormatter : std::runtime_error { @@ -68,8 +69,6 @@ VirtualMachine::UPtr HyperVAPIVirtualMachineFactory::create_virtual_machine(cons auto hcn = std::make_unique(); auto virtdisk = std::make_unique(); - static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; - return std::make_unique(std::move(hcs), std::move(hcn), std::move(virtdisk), @@ -159,4 +158,4 @@ std::string HyperVAPIVirtualMachineFactory::create_bridge_with(const NetworkInte return {}; } -} // namespace multipass::hyperv \ No newline at end of file +} // namespace multipass::hyperv From ccc037a3d07082920f34dc0506d698c947dd1e06 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 10 Mar 2025 13:45:01 +0300 Subject: [PATCH 12/76] [hyperv-api-vm] more logs & tidyup --- .../hyperv_api/hyperv_api_virtual_machine.cpp | 132 ++++++++++++++---- 1 file changed, 107 insertions(+), 25 deletions(-) diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp index c756bb7cde..81db85d39c 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -35,6 +35,7 @@ namespace * Category for the log messages. */ constexpr auto kLogCategory = "HyperV-Virtual-Machine"; +constexpr auto kDefaultSSHPort = 22; namespace mpl = multipass::logging; using lvl = mpl::Level; @@ -55,16 +56,28 @@ inline auto replace_colon_with_dash(std::string& addr) std::replace(addr.begin(), addr.end(), ':', '-'); } +/** + * 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) { + mpl::log(lvl::debug, kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`"); static auto wsa_context = [] { struct wsa_init_wrapper { - wsa_init_wrapper() : wsa_data{}, wsa_init_success(WSAStartup(MAKEWORD(2, 2), &wsa_data)) + wsa_init_wrapper() : wsa_data{}, wsa_init_success(WSAStartup(MAKEWORD(2, 2) == 0, &wsa_data)) { + mpl::log(lvl::debug, + kLogCategory, + "resolve_ip_addresses() -> initialized WSA, status `{}`", + wsa_init_success); if (!wsa_init_success) { - mpl::log(lvl::error, kLogCategory, "resolve_and_memoize_ip_addresses() > WSAStartup failed!"); + mpl::log(lvl::error, kLogCategory, "resolve_ip_addresses() > WSAStartup failed!"); } } ~wsa_init_wrapper() @@ -96,6 +109,7 @@ auto resolve_ip_addresses(const std::string& hostname) }(); std::vector ipv4{}, ipv6{}; + if (result == 0) { assert(addr_info.get()); @@ -105,18 +119,41 @@ auto resolve_ip_addresses(const std::string& hostname) { case AF_INET: { - 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); + constexpr auto kSockaddrInSize = sizeof(std::remove_pointer_t); + if (ptr->ai_addrlen >= kSockaddrInSize) + { + 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::log( + lvl::error, + kLogCategory, + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv4 address data while expecting {}!", + ptr->ai_addrlen, + kSockaddrInSize); } break; case AF_INET6: { - const auto sockaddr_ipv6 = reinterpret_cast(ptr->ai_addr); - char addr[INET6_ADDRSTRLEN] = {}; - inet_ntop(AF_INET6, &(sockaddr_ipv6->sa_data), addr, sizeof(addr)); - ipv6.push_back(addr); + constexpr auto kSockaddrIn6Size = sizeof(std::remove_pointer_t); + if (ptr->ai_addrlen >= kSockaddrIn6Size) + { + 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::log( + lvl::error, + kLogCategory, + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv6 address data while expecting {}!", + ptr->ai_addrlen, + kSockaddrIn6Size); } break; default: @@ -124,6 +161,13 @@ auto resolve_ip_addresses(const std::string& hostname) } } } + + mpl::log(lvl::debug, + kLogCategory, + "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", + fmt::join(ipv4, ","), + fmt::join(ipv6, ",")); + return std::make_pair(ipv4, ipv6); } } // namespace @@ -263,7 +307,8 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) { - fmt::print(L"{}", create_result.status_msg); + + fmt::print(L"Create compute system failed: {}", create_result.status_msg); throw CreateComputeSystemException{}; } @@ -331,29 +376,34 @@ void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_s void HyperVAPIVirtualMachine::start() { - mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, state {}", vm_name, state); + mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, current state {}", vm_name, state); state = VirtualMachine::State::starting; update_state(); // Resume and start are the same thing in Multipass terms // Try to determine whether we need to resume or start here. const auto& [status, status_msg] = [&] { // Fetch the latest state value. - if (fetch_state_from_api() == hcs::ComputeSystemState::paused) + const auto hcs_state = fetch_state_from_api(); + switch (hcs_state) + { + case hcs::ComputeSystemState::paused: { mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in paused state, resuming", vm_name); return hcs->resume_compute_system(vm_name); } - mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); - return hcs->start_compute_system(vm_name); + case hcs::ComputeSystemState::created: + [[fallthrough]]; + default: + { + mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); + return hcs->start_compute_system(vm_name); + } + } }(); - - // // Maybe wait until SSH is up with timeout - // wait_until_ssh_up(std::chrono::seconds{240}); - // set_state(fetch_state_from_api()); - // update_state(); } void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { + mpl::log(lvl::debug, kLogCategory, "shutdown() -> Shutting down VM `{}`, current state {}", vm_name, state); switch (shutdown_policy) { @@ -363,11 +413,18 @@ void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall, // some way to signal "vmwp.exe" for a graceful shutdown, sysrq via console // or other "direct" means to trigger the shutdown. + mpl::log(lvl::debug, + kLogCategory, + "shutdown() -> Requested powerdown, initiating graceful shutdown for `{}`", + vm_name); ssh_exec("sudo shutdown -h now"); - drop_ssh_session(); break; case ShutdownPolicy::Halt: case ShutdownPolicy::Poweroff: + mpl::log(lvl::debug, + kLogCategory, + "shutdown() -> Requested halt/poweroff, initiating forceful shutdown for `{}`", + vm_name); drop_ssh_session(); // These are non-graceful variants. Just terminate the system immediately. hcs->terminate_compute_system(vm_name); @@ -380,6 +437,7 @@ void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) void HyperVAPIVirtualMachine::suspend() { + mpl::log(lvl::debug, kLogCategory, "suspend() -> Suspending VM `{}`, current state {}", vm_name, state); const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); set_state(fetch_state_from_api()); update_state(); @@ -391,7 +449,6 @@ HyperVAPIVirtualMachine::State HyperVAPIVirtualMachine::current_state() } int HyperVAPIVirtualMachine::ssh_port() { - constexpr auto kDefaultSSHPort = 22; return kDefaultSSHPort; } std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) @@ -408,13 +465,13 @@ std::string HyperVAPIVirtualMachine::management_ipv4() const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv4.empty()) { - // TODO: Log - return {}; + mpl::log(lvl::error, kLogCategory, "management_ipv4() > failed to resolve `{}`", ssh_hostname({})); + return "UNKNOWN"; } const auto result = *ipv4.begin(); - mpl::log(lvl::info, kLogCategory, "management_ipv4() > IP address is `{}`", result); + mpl::log(lvl::trace, kLogCategory, "management_ipv4() > IP address is `{}`", result); // Prefer the first one return result; } @@ -448,25 +505,50 @@ hcs::ComputeSystemState HyperVAPIVirtualMachine::fetch_state_from_api() void HyperVAPIVirtualMachine::update_cpus(int num_cores) { + mpl::log(lvl::debug, kLogCategory, "update_cpus() -> called for VM `{}`, num_cores `{}`", vm_name, num_cores); + throw std::runtime_error{"Not yet implemented"}; } void HyperVAPIVirtualMachine::resize_memory(const MemorySize& new_size) { + mpl::log(lvl::debug, + kLogCategory, + "resize_memory() -> called for VM `{}`, new_size `{}` MiB", + vm_name, + new_size.in_megabytes()); + const auto& [status, status_msg] = hcs->resize_memory(vm_name, new_size.in_megabytes()); } void HyperVAPIVirtualMachine::resize_disk(const MemorySize& new_size) { + mpl::log(lvl::debug, + kLogCategory, + "resize_disk() -> called for VM `{}`, new_size `{}` MiB", + vm_name, + new_size.in_megabytes()); throw std::runtime_error{"Not yet implemented"}; } void HyperVAPIVirtualMachine::add_network_interface(int index, const std::string& default_mac_addr, const NetworkInterface& extra_interface) { + mpl::log(lvl::debug, + kLogCategory, + "add_network_interface() -> called for VM `{}`, index: {}, default_mac: {}, extra_interface: (mac: {}, " + "auto_mode: {}, id: {})" + "auto_mode: {}, ", + vm_name, + index, + default_mac_addr, + extra_interface.mac_address, + extra_interface.auto_mode, + extra_interface.id); throw std::runtime_error{"Not yet implemented"}; } std::unique_ptr HyperVAPIVirtualMachine::make_native_mount_handler(const std::string& target, const VMMount& mount) { + mpl::log(lvl::debug, kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); throw std::runtime_error{"Not yet implemented"}; } From 3482489482a858407ebc24f81737594aebd4b369 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 10 Mar 2025 13:55:28 +0300 Subject: [PATCH 13/76] [hyperv-api-vm] exceptions: switch to FormattedExceptionBase<> --- .../hyperv_api/hyperv_api_virtual_machine.cpp | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp index 81db85d39c..f1a4564563 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp @@ -18,6 +18,7 @@ #include "hyperv_api_virtual_machine.h" #include "hcn/hyperv_hcn_create_endpoint_params.h" #include "hcs/hyperv_hcs_compute_system_state.h" +#include #include #include #include @@ -175,41 +176,29 @@ auto resolve_ip_addresses(const std::string& hostname) namespace multipass::hyperv { -struct ExceptionFormatter : std::runtime_error +struct InvalidAPIPointerException : FormattedExceptionBase<> { - template - ExceptionFormatter(fmt::format_string fmt, Args&&... args) - : std::runtime_error{fmt::format(fmt, std::forward(args)...)} - { - } - ExceptionFormatter() : std::runtime_error("") - { - } -}; - -struct InvalidAPIPointerException : ExceptionFormatter -{ - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; -struct CreateComputeSystemException : ExceptionFormatter +struct CreateComputeSystemException : FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; -struct ComputeSystemStateException : ExceptionFormatter +struct ComputeSystemStateException : FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; -struct CreateEndpointException : ExceptionFormatter +struct CreateEndpointException : FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; -struct GrantVMAccessException : ExceptionFormatter +struct GrantVMAccessException : FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, @@ -229,10 +218,11 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, { // Verify that the given API wrappers are not null { - const void* api_ptrs[] = {hcs.get(), hcn.get(), virtdisk.get()}; + const std::array api_ptrs = {hcs.get(), hcn.get(), virtdisk.get()}; if (std::any_of(std::begin(api_ptrs), std::end(api_ptrs), [](const void* ptr) { return nullptr == ptr; })) { - throw InvalidAPIPointerException{}; + throw InvalidAPIPointerException{"One of the required API pointers is not set: {}.", + fmt::join(api_ptrs, ",")}; } } @@ -268,7 +258,7 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, } if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) { - throw CreateEndpointException{}; + throw CreateEndpointException{"create_endpoint failed with {}", status}; } } @@ -309,7 +299,7 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, { fmt::print(L"Create compute system failed: {}", create_result.status_msg); - throw CreateComputeSystemException{}; + throw CreateComputeSystemException{"create_compute_system failed with {}", create_result}; } // Grant access to the VHDX and the cloud-init ISO files. From 8dabcb692b691eede25f299a33ef47c005dc0e0b Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 10 Mar 2025 15:00:11 +0300 Subject: [PATCH 14/76] [hyperv-api-vm] replace factory/vm hyperv_api_ pfx with hcs_ Hyper-V API has been the umbrella term to call the technology behind this feature, but it might be misleading because there's actually a real Hyper-V API and the feature does not use that. Instead, the feature utilizes Host Compute System and Host Compute Network API's. It's better to call this backend as what it actually is. --- .../backends/hyperv_api/CMakeLists.txt | 4 +- ...al_machine.cpp => hcs_virtual_machine.cpp} | 42 +++++++++---------- ...irtual_machine.h => hcs_virtual_machine.h} | 6 +-- ...ry.cpp => hcs_virtual_machine_factory.cpp} | 38 ++++++++--------- ...actory.h => hcs_virtual_machine_factory.h} | 8 ++-- src/platform/platform_win.cpp | 4 +- 6 files changed, 51 insertions(+), 51 deletions(-) rename src/platform/backends/hyperv_api/{hyperv_api_virtual_machine.cpp => hcs_virtual_machine.cpp} (93%) rename src/platform/backends/hyperv_api/{hyperv_api_virtual_machine.h => hcs_virtual_machine.h} (95%) rename src/platform/backends/hyperv_api/{hyperv_api_virtual_machine_factory.cpp => hcs_virtual_machine_factory.cpp} (75%) rename src/platform/backends/hyperv_api/{hyperv_api_virtual_machine_factory.h => hcs_virtual_machine_factory.h} (87%) diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 72d447357a..7213e47a24 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -45,8 +45,8 @@ if(WIN32) hcn/hyperv_hcn_api_wrapper.cpp hcs/hyperv_hcs_api_wrapper.cpp virtdisk/virtdisk_api_wrapper.cpp - hyperv_api_virtual_machine.cpp - hyperv_api_virtual_machine_factory.cpp + hcs_virtual_machine.cpp + hcs_virtual_machine_factory.cpp ) target_link_libraries(hyperv_api_backend PRIVATE diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp similarity index 93% rename from src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp rename to src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index f1a4564563..fddc08ced8 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -15,7 +15,7 @@ * */ -#include "hyperv_api_virtual_machine.h" +#include "hcs_virtual_machine.h" #include "hcn/hyperv_hcn_create_endpoint_params.h" #include "hcs/hyperv_hcs_compute_system_state.h" #include @@ -201,7 +201,7 @@ struct GrantVMAccessException : FormattedExceptionBase<> using FormattedExceptionBase<>::FormattedExceptionBase; }; -HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, +HCSVirtualMachine::HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, unique_hcn_wrapper_t hcn_w, unique_virtdisk_wrapper_t virtdisk_w, const std::string& network_guid, @@ -321,7 +321,7 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, update_state(); } -// HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(const std::string& source_vm_name, +// HCSVirtualMachine::HCSVirtualMachine(const std::string& source_vm_name, // const multipass::VMSpecs& src_vm_specs, // const VirtualMachineDescription& desc, // VMStatusMonitor& monitor, @@ -330,7 +330,7 @@ HyperVAPIVirtualMachine::HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, // { // } -void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) +void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) { const auto prev_state = state; switch (compute_system_state) @@ -364,7 +364,7 @@ void HyperVAPIVirtualMachine::set_state(hcs::ComputeSystemState compute_system_s } } -void HyperVAPIVirtualMachine::start() +void HCSVirtualMachine::start() { mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, current state {}", vm_name, state); state = VirtualMachine::State::starting; @@ -391,7 +391,7 @@ void HyperVAPIVirtualMachine::start() } }(); } -void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) +void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { mpl::log(lvl::debug, kLogCategory, "shutdown() -> Shutting down VM `{}`, current state {}", vm_name, state); @@ -425,7 +425,7 @@ void HyperVAPIVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) update_state(); } -void HyperVAPIVirtualMachine::suspend() +void HCSVirtualMachine::suspend() { mpl::log(lvl::debug, kLogCategory, "suspend() -> Suspending VM `{}`, current state {}", vm_name, state); const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); @@ -433,24 +433,24 @@ void HyperVAPIVirtualMachine::suspend() update_state(); } -HyperVAPIVirtualMachine::State HyperVAPIVirtualMachine::current_state() +HCSVirtualMachine::State HCSVirtualMachine::current_state() { return state; } -int HyperVAPIVirtualMachine::ssh_port() +int HCSVirtualMachine::ssh_port() { return kDefaultSSHPort; } -std::string HyperVAPIVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) +std::string HCSVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) { return fmt::format("{}.mshome.net", vm_name); } -std::string HyperVAPIVirtualMachine::ssh_username() +std::string HCSVirtualMachine::ssh_username() { return description.ssh_username; } -std::string HyperVAPIVirtualMachine::management_ipv4() +std::string HCSVirtualMachine::management_ipv4() { const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv4.empty()) @@ -465,7 +465,7 @@ std::string HyperVAPIVirtualMachine::management_ipv4() // Prefer the first one return result; } -std::string HyperVAPIVirtualMachine::ipv6() +std::string HCSVirtualMachine::ipv6() { const auto& [_, ipv6] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv6.empty()) @@ -476,30 +476,30 @@ std::string HyperVAPIVirtualMachine::ipv6() // Prefer the first one return *ipv6.begin(); } -void HyperVAPIVirtualMachine::ensure_vm_is_running() +void HCSVirtualMachine::ensure_vm_is_running() { auto is_vm_running = [this] { return state != State::off; }; multipass::backend::ensure_vm_is_running_for(this, is_vm_running, "Instance shutdown during start"); } -void HyperVAPIVirtualMachine::update_state() +void HCSVirtualMachine::update_state() { monitor.persist_state_for(vm_name, state); } -hcs::ComputeSystemState HyperVAPIVirtualMachine::fetch_state_from_api() +hcs::ComputeSystemState HCSVirtualMachine::fetch_state_from_api() { hcs::ComputeSystemState compute_system_state{hcs::ComputeSystemState::unknown}; const auto result = hcs->get_compute_system_state(vm_name, compute_system_state); return compute_system_state; } -void HyperVAPIVirtualMachine::update_cpus(int num_cores) +void HCSVirtualMachine::update_cpus(int num_cores) { mpl::log(lvl::debug, kLogCategory, "update_cpus() -> called for VM `{}`, num_cores `{}`", vm_name, num_cores); throw std::runtime_error{"Not yet implemented"}; } -void HyperVAPIVirtualMachine::resize_memory(const MemorySize& new_size) +void HCSVirtualMachine::resize_memory(const MemorySize& new_size) { mpl::log(lvl::debug, kLogCategory, @@ -509,7 +509,7 @@ void HyperVAPIVirtualMachine::resize_memory(const MemorySize& new_size) const auto& [status, status_msg] = hcs->resize_memory(vm_name, new_size.in_megabytes()); } -void HyperVAPIVirtualMachine::resize_disk(const MemorySize& new_size) +void HCSVirtualMachine::resize_disk(const MemorySize& new_size) { mpl::log(lvl::debug, kLogCategory, @@ -518,7 +518,7 @@ void HyperVAPIVirtualMachine::resize_disk(const MemorySize& new_size) new_size.in_megabytes()); throw std::runtime_error{"Not yet implemented"}; } -void HyperVAPIVirtualMachine::add_network_interface(int index, +void HCSVirtualMachine::add_network_interface(int index, const std::string& default_mac_addr, const NetworkInterface& extra_interface) { @@ -535,7 +535,7 @@ void HyperVAPIVirtualMachine::add_network_interface(int index, extra_interface.id); throw std::runtime_error{"Not yet implemented"}; } -std::unique_ptr HyperVAPIVirtualMachine::make_native_mount_handler(const std::string& target, +std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const std::string& target, const VMMount& mount) { mpl::log(lvl::debug, kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h similarity index 95% rename from src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h rename to src/platform/backends/hyperv_api/hcs_virtual_machine.h index 2423aa5161..914408b8b8 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -37,13 +37,13 @@ namespace multipass::hyperv /** * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. */ -struct HyperVAPIVirtualMachine final : public BaseVirtualMachine +struct HCSVirtualMachine final : public BaseVirtualMachine { using unique_hcn_wrapper_t = std::unique_ptr; using unique_hcs_wrapper_t = std::unique_ptr; using unique_virtdisk_wrapper_t = std::unique_ptr; - HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, + HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, unique_hcn_wrapper_t hcn_w, unique_virtdisk_wrapper_t virtdisk_w, const std::string& network_guid, @@ -52,7 +52,7 @@ struct HyperVAPIVirtualMachine final : public BaseVirtualMachine const SSHKeyProvider& key_provider, const Path& instance_dir); - HyperVAPIVirtualMachine(unique_hcs_wrapper_t hcs_w, + HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, unique_hcn_wrapper_t hcn_w, unique_virtdisk_wrapper_t virtdisk_w, const std::string& source_vm_name, diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp similarity index 75% rename from src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp rename to src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 16a4f1aae6..76f6a78ade 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -15,8 +15,8 @@ * */ -#include "hyperv_api_virtual_machine_factory.h" -#include "hyperv_api_virtual_machine.h" +#include "hcs_virtual_machine_factory.h" +#include "hcs_virtual_machine.h" #include #include @@ -56,35 +56,35 @@ struct ImageResizeException : public ExceptionFormatter using ExceptionFormatter::ExceptionFormatter; }; -HyperVAPIVirtualMachineFactory::HyperVAPIVirtualMachineFactory(const Path& data_dir) +HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) { } -VirtualMachine::UPtr HyperVAPIVirtualMachineFactory::create_virtual_machine(const VirtualMachineDescription& desc, - const SSHKeyProvider& key_provider, - VMStatusMonitor& monitor) +VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine(const VirtualMachineDescription& desc, + const SSHKeyProvider& key_provider, + VMStatusMonitor& monitor) { auto hcs = std::make_unique(); auto hcn = std::make_unique(); auto virtdisk = std::make_unique(); - return std::make_unique(std::move(hcs), - std::move(hcn), - std::move(virtdisk), - kDefaultHyperVSwitchGUID, - desc, - monitor, - key_provider, - get_instance_directory(desc.vm_name)); + return std::make_unique(std::move(hcs), + std::move(hcn), + std::move(virtdisk), + kDefaultHyperVSwitchGUID, + desc, + monitor, + key_provider, + get_instance_directory(desc.vm_name)); } -void HyperVAPIVirtualMachineFactory::remove_resources_for_impl(const std::string& name) +void HCSVirtualMachineFactory::remove_resources_for_impl(const std::string& name) { throw std::runtime_error{"Not implemented yet."}; } -VMImage HyperVAPIVirtualMachineFactory::prepare_source_image(const VMImage& source_image) +VMImage HCSVirtualMachineFactory::prepare_source_image(const VMImage& source_image) { const std::filesystem::path source_file{source_image.image_path.toStdString()}; @@ -134,8 +134,8 @@ VMImage HyperVAPIVirtualMachineFactory::prepare_source_image(const VMImage& sour return result; } -void HyperVAPIVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, - const VirtualMachineDescription& desc) +void HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, + const VirtualMachineDescription& desc) { // FIXME: virtdisk::VirtDiskWrapper wrap{}; @@ -151,7 +151,7 @@ void HyperVAPIVirtualMachineFactory::prepare_instance_image(const VMImage& insta } } -std::string HyperVAPIVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) +std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) { (void)intf; // No-op. The implementation uses the default Hyper-V switch. diff --git a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h similarity index 87% rename from src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h rename to src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h index 685286b470..5ce1b4a1b6 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_virtual_machine_factory.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -15,8 +15,8 @@ * */ -#ifndef MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_FACTORY_H -#define MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_FACTORY_H +#ifndef MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_FACTORY_H +#define MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_FACTORY_H #include @@ -26,9 +26,9 @@ namespace multipass::hyperv /** * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. */ -struct HyperVAPIVirtualMachineFactory final : public BaseVirtualMachineFactory +struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory { - explicit HyperVAPIVirtualMachineFactory(const Path& data_dir); + explicit HCSVirtualMachineFactory(const Path& data_dir); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 87950cdd02..d914d4cd01 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -27,7 +27,7 @@ #include #include "backends/hyperv/hyperv_virtual_machine_factory.h" -#include "backends/hyperv_api/hyperv_api_virtual_machine_factory.h" +#include "backends/hyperv_api/hcs_virtual_machine_factory.h" #include "backends/virtualbox/virtualbox_virtual_machine_factory.h" #include "logger/win_event_logger.h" #include "shared/sshfs_server_process_spec.h" @@ -619,7 +619,7 @@ mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_di } else if (driver == "hyperv_api") { - return std::make_unique(data_dir); + return std::make_unique(data_dir); } throw std::runtime_error("Invalid virtualization driver set in the environment"); From 9024b353530c88918c3f0e8f7390438a2bca36e6 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 10 Mar 2025 16:06:59 +0300 Subject: [PATCH 15/76] [hyperv-api-factory] exceptions: switch to FormattedExceptionBase<> --- .../hcs_virtual_machine_factory.cpp | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 76f6a78ade..73315e0c96 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -18,8 +18,10 @@ #include "hcs_virtual_machine_factory.h" #include "hcs_virtual_machine.h" #include +#include #include + #include #include // for the std::filesystem::path formatter @@ -33,27 +35,14 @@ namespace multipass::hyperv static constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; -struct ExceptionFormatter : std::runtime_error -{ - template - ExceptionFormatter(fmt::format_string fmt, Args&&... args) - : std::runtime_error{fmt::format(fmt, std::forward(args)...)} - { - } - - ExceptionFormatter() : std::runtime_error("") - { - } -}; - -struct ImageConversionException : public ExceptionFormatter +struct ImageConversionException : public FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; -struct ImageResizeException : public ExceptionFormatter +struct ImageResizeException : public FormattedExceptionBase<> { - using ExceptionFormatter::ExceptionFormatter; + using FormattedExceptionBase<>::FormattedExceptionBase; }; HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) From 98b5f178af0da5fa18b8c94f85874126cfcdf5fd Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 10 Mar 2025 17:11:34 +0300 Subject: [PATCH 16/76] [hyperv-api-vm|factory] more grooming - api wrappers are now constructed at factory - factory and vm now use the interface types instead of concrete wrappers - switched to <> style includes - moved exception types to hcs_virtual_machine_exception.h --- .../hcs/hyperv_hcs_compute_system_state.h | 41 +++ .../hyperv_api/hcs_virtual_machine.cpp | 246 +++++++++--------- .../backends/hyperv_api/hcs_virtual_machine.h | 58 ++--- .../hcs_virtual_machine_exceptions.h | 63 +++++ .../hcs_virtual_machine_factory.cpp | 60 +++-- .../hyperv_api/hcs_virtual_machine_factory.h | 8 + .../hyperv_api/hyperv_api_wrapper_fwdecl.h | 46 ++++ 7 files changed, 341 insertions(+), 181 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h create mode 100644 src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h index 96f205dc72..334d7e88c9 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h @@ -71,4 +71,45 @@ inline std::optional compute_system_state_from_string(std::s } // namespace multipass::hyperv::hcs +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(multipass::hyperv::hcs::ComputeSystemState state, FormatContext& ctx) const + { + std::string_view v = "(undefined)"; + switch (state) + { + case multipass::hyperv::hcs::ComputeSystemState::created: + v = "created"; + break; + case multipass::hyperv::hcs::ComputeSystemState::paused: + v = "paused"; + break; + case multipass::hyperv::hcs::ComputeSystemState::running: + v = "running"; + break; + case multipass::hyperv::hcs::ComputeSystemState::saved_as_template: + v = "saved_as_template"; + break; + case multipass::hyperv::hcs::ComputeSystemState::stopped: + v = "stopped"; + break; + case multipass::hyperv::hcs::ComputeSystemState::unknown: + v = "unknown"; + break; + } + + return format_to(ctx.out(), "{}", v); + } +}; + #endif diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index fddc08ced8..d741bfea97 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -15,9 +15,15 @@ * */ -#include "hcs_virtual_machine.h" -#include "hcn/hyperv_hcn_create_endpoint_params.h" -#include "hcs/hyperv_hcs_compute_system_state.h" +#include + +#include +#include +#include +#include +#include +#include + #include #include #include @@ -66,7 +72,7 @@ inline auto replace_colon_with_dash(std::string& addr) */ auto resolve_ip_addresses(const std::string& hostname) { - mpl::log(lvl::debug, kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`"); + mpl::log(lvl::trace, kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`", hostname); static auto wsa_context = [] { struct wsa_init_wrapper { @@ -163,9 +169,10 @@ auto resolve_ip_addresses(const std::string& hostname) } } - mpl::log(lvl::debug, + mpl::log(lvl::trace, kLogCategory, "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", + hostname, fmt::join(ipv4, ","), fmt::join(ipv6, ",")); @@ -176,41 +183,17 @@ auto resolve_ip_addresses(const std::string& hostname) namespace multipass::hyperv { -struct InvalidAPIPointerException : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -struct CreateComputeSystemException : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -struct ComputeSystemStateException : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -struct CreateEndpointException : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -struct GrantVMAccessException : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -HCSVirtualMachine::HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, - unique_hcn_wrapper_t hcn_w, - unique_virtdisk_wrapper_t virtdisk_w, - const std::string& network_guid, - const VirtualMachineDescription& desc, - class VMStatusMonitor& monitor, - const SSHKeyProvider& key_provider, - const Path& instance_dir) +HCSVirtualMachine::HCSVirtualMachine(hcs_sptr_t hcs_w, + hcn_sptr_t hcn_w, + virtdisk_sptr_t virtdisk_w, + const std::string& network_guid, + const VirtualMachineDescription& desc, + class VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir) : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, description(desc), + primary_network_guid(network_guid), hcs(std::move(hcs_w)), hcn(std::move(hcn_w)), monitor(monitor), @@ -226,112 +209,110 @@ HCSVirtualMachine::HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, } } + // Craete + maybe_create_compute_system(); + // Reflect compute system's state + set_state(fetch_state_from_api()); + update_state(); +} + +void HCSVirtualMachine::maybe_create_compute_system() +{ hcs::ComputeSystemState cs_state{hcs::ComputeSystemState::unknown}; - { - // Check if the VM already exist - const auto result = hcs->get_compute_system_state(vm_name, cs_state); + // Check if the VM already exist + const auto result = hcs->get_compute_system_state(vm_name, cs_state); - if (E_INVALIDARG == static_cast(result.code)) + if (E_INVALIDARG == static_cast(result.code)) + { + // FIXME: Handle suspend state? + + const auto endpoint_params = [this]() { + std::vector endpoint_params{}; + endpoint_params.emplace_back( + hcn::CreateEndpointParameters{primary_network_guid, mac2uuid(description.default_mac_address)}); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); + // } + return endpoint_params; + }(); + + for (const auto& endpoint : endpoint_params) { - - const auto endpoint_params = [&desc, &network_guid]() { - std::vector endpoint_params{}; - endpoint_params.emplace_back( - hcn::CreateEndpointParameters{network_guid, mac2uuid(desc.default_mac_address)}); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); - // } - return endpoint_params; - }(); - - for (const auto& endpoint : endpoint_params) + // There might be remnants from an old VM, remove the endpoint if exist before + // creating it again. + if (hcn->delete_endpoint(endpoint.endpoint_guid)) { - // There might be remnants from an old VM, remove the endpoint if exist before - // creating it again. - if (hcn->delete_endpoint(endpoint.endpoint_guid)) - { - // TODO: log - } - if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) - { - throw CreateEndpointException{"create_endpoint failed with {}", status}; - } + // TODO: log } - - // E_INVALIDARG means there's no such VM. - // Create the VM from scratch. - const auto ccs_params = [&desc, &endpoint_params]() { - hcs::CreateComputeSystemParameters ccs_params{}; - ccs_params.name = desc.vm_name; - ccs_params.memory_size_mb = desc.mem_size.in_megabytes(); - ccs_params.processor_count = desc.num_cores; - ccs_params.cloudinit_iso_path = desc.cloud_init_iso.toStdString(); - ccs_params.vhdx_path = desc.image.image_path.toStdString(); - - hcs::AddEndpointParameters default_endpoint_params{}; - default_endpoint_params.nic_mac_address = desc.default_mac_address; - // Hyper-V API does not like colons. Ensure that the MAC is separated - // with dash instead of colon. - replace_colon_with_dash(default_endpoint_params.nic_mac_address); - // make the UUID deterministic so we can query the endpoint with a MAC address - // if needed. - default_endpoint_params.endpoint_guid = mac2uuid(desc.default_mac_address); - default_endpoint_params.target_compute_system_name = ccs_params.name; - ccs_params.endpoints.push_back(default_endpoint_params); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // hcs::AddEndpointParameters endpoint_params{}; - // endpoint_params.nic_mac_address = v.mac_address; - // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); - // endpoint_params.target_compute_system_name = ccs_params.name; - // ccs_params.endpoints.push_back(endpoint_params); - // } - return ccs_params; - }(); - - if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) { - - fmt::print(L"Create compute system failed: {}", create_result.status_msg); - throw CreateComputeSystemException{"create_compute_system failed with {}", create_result}; + throw CreateEndpointException{"create_endpoint failed with {}", status}; } + } + + // E_INVALIDARG means there's no such VM. + // Create the VM from scratch. + const auto ccs_params = [this, &endpoint_params]() { + hcs::CreateComputeSystemParameters ccs_params{}; + ccs_params.name = description.vm_name; + ccs_params.memory_size_mb = description.mem_size.in_megabytes(); + ccs_params.processor_count = description.num_cores; + ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); + ccs_params.vhdx_path = description.image.image_path.toStdString(); + + hcs::AddEndpointParameters default_endpoint_params{}; + default_endpoint_params.nic_mac_address = description.default_mac_address; + // Hyper-V API does not like colons. Ensure that the MAC is separated + // with dash instead of colon. + replace_colon_with_dash(default_endpoint_params.nic_mac_address); + // make the UUID deterministic so we can query the endpoint with a MAC address + // if needed. + default_endpoint_params.endpoint_guid = mac2uuid(description.default_mac_address); + default_endpoint_params.target_compute_system_name = ccs_params.name; + ccs_params.endpoints.push_back(default_endpoint_params); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // hcs::AddEndpointParameters endpoint_params{}; + // endpoint_params.nic_mac_address = v.mac_address; + // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); + // endpoint_params.target_compute_system_name = ccs_params.name; + // ccs_params.endpoints.push_back(endpoint_params); + // } + return ccs_params; + }(); + + if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + { + + fmt::print(L"Create compute system failed: {}", create_result.status_msg); + throw CreateComputeSystemException{"create_compute_system failed with {}", create_result}; + } - // Grant access to the VHDX and the cloud-init ISO files. - const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; + // Grant access to the VHDX and the cloud-init ISO files. + const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; - for (const auto& path : grant_paths) + for (const auto& path : grant_paths) + { + if (!hcs->grant_vm_access(ccs_params.name, path)) { - if (!hcs->grant_vm_access(ccs_params.name, path)) - { - throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", - ccs_params.name, - path}; - } + throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", + ccs_params.name, + path}; } } } - // Reflect compute system's state - set_state(fetch_state_from_api()); - update_state(); } -// HCSVirtualMachine::HCSVirtualMachine(const std::string& source_vm_name, -// const multipass::VMSpecs& src_vm_specs, -// const VirtualMachineDescription& desc, -// VMStatusMonitor& monitor, -// const SSHKeyProvider& key_provider, -// const Path& dest_instance_dir) -// { -// } - void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) { + mpl::log(lvl::debug, kLogCategory, "set_state() -> VM `{}` HCS state `{}`", vm_name, compute_system_state); + const auto prev_state = state; switch (compute_system_state) { @@ -367,6 +348,10 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) void HCSVirtualMachine::start() { mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, current state {}", vm_name, state); + + // Create the compute system, if not created yet. + maybe_create_compute_system(); + state = VirtualMachine::State::starting; update_state(); // Resume and start are the same thing in Multipass terms @@ -374,6 +359,7 @@ void HCSVirtualMachine::start() const auto& [status, status_msg] = [&] { // Fetch the latest state value. const auto hcs_state = fetch_state_from_api(); + mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` HCS state is `{}`", vm_name, hcs_state); switch (hcs_state) { case hcs::ComputeSystemState::paused: @@ -390,6 +376,8 @@ void HCSVirtualMachine::start() } } }(); + + mpl::log(lvl::debug, kLogCategory, "start() -> Start/resume VM `{}`, result `{}`", vm_name, status); } void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { @@ -519,8 +507,8 @@ void HCSVirtualMachine::resize_disk(const MemorySize& new_size) throw std::runtime_error{"Not yet implemented"}; } void HCSVirtualMachine::add_network_interface(int index, - const std::string& default_mac_addr, - const NetworkInterface& extra_interface) + const std::string& default_mac_addr, + const NetworkInterface& extra_interface) { mpl::log(lvl::debug, kLogCategory, @@ -536,7 +524,7 @@ void HCSVirtualMachine::add_network_interface(int index, throw std::runtime_error{"Not yet implemented"}; } std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const std::string& target, - const VMMount& mount) + const VMMount& mount) { mpl::log(lvl::debug, kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); throw std::runtime_error{"Not yet implemented"}; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h index 914408b8b8..b28b8b2f32 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -18,9 +18,9 @@ #ifndef MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H #define MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H -#include "hcn/hyperv_hcn_api_wrapper.h" -#include "hcs/hyperv_hcs_api_wrapper.h" -#include "virtdisk/virtdisk_api_wrapper.h" +#include +#include + #include #include @@ -39,28 +39,24 @@ namespace multipass::hyperv */ struct HCSVirtualMachine final : public BaseVirtualMachine { - using unique_hcn_wrapper_t = std::unique_ptr; - using unique_hcs_wrapper_t = std::unique_ptr; - using unique_virtdisk_wrapper_t = std::unique_ptr; - - HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, - unique_hcn_wrapper_t hcn_w, - unique_virtdisk_wrapper_t virtdisk_w, - const std::string& network_guid, - const VirtualMachineDescription& desc, - VMStatusMonitor& monitor, - const SSHKeyProvider& key_provider, - const Path& instance_dir); + HCSVirtualMachine(hcs_sptr_t hcs_w, + hcn_sptr_t hcn_w, + virtdisk_sptr_t virtdisk_w, + const std::string& network_guid, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir); - HCSVirtualMachine(unique_hcs_wrapper_t hcs_w, - unique_hcn_wrapper_t hcn_w, - unique_virtdisk_wrapper_t virtdisk_w, - const std::string& source_vm_name, - const multipass::VMSpecs& src_vm_specs, - const VirtualMachineDescription& desc, - VMStatusMonitor& monitor, - const SSHKeyProvider& key_provider, - const Path& dest_instance_dir); + HCSVirtualMachine(hcs_sptr_t hcs_w, + hcn_sptr_t hcn_w, + virtdisk_sptr_t virtdisk_w, + const std::string& source_vm_name, + const multipass::VMSpecs& src_vm_specs, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& dest_instance_dir); void start() override; void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override; @@ -82,18 +78,22 @@ struct HCSVirtualMachine final : public BaseVirtualMachine std::unique_ptr make_native_mount_handler(const std::string& target, const VMMount& mount) override; protected: - void require_snapshots_support() const override{} + void require_snapshots_support() const override + { + } private: - const VirtualMachineDescription description; - unique_hcs_wrapper_t hcs{nullptr}; - unique_hcn_wrapper_t hcn{nullptr}; - unique_virtdisk_wrapper_t virtdisk{nullptr}; + const VirtualMachineDescription description{}; + const std::string primary_network_guid{}; + hcs_sptr_t hcs{nullptr}; + hcn_sptr_t hcn{nullptr}; + virtdisk_sptr_t virtdisk{nullptr}; VMStatusMonitor& monitor; hcs::ComputeSystemState fetch_state_from_api(); void set_state(hcs::ComputeSystemState state); + void maybe_create_compute_system(); }; } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h new file mode 100644 index 0000000000..bd470a3b5f --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h @@ -0,0 +1,63 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_EXCEPTIONS_H +#define MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_EXCEPTIONS_H + +#include + +namespace multipass::hyperv +{ + +struct InvalidAPIPointerException : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct CreateComputeSystemException : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct ComputeSystemStateException : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct CreateEndpointException : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct GrantVMAccessException : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct ImageConversionException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct ImageResizeException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +} // namespace multipass::hyperv + +#endif diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 73315e0c96..03817aa5bc 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -15,13 +15,17 @@ * */ -#include "hcs_virtual_machine_factory.h" -#include "hcs_virtual_machine.h" +#include + +#include +#include +#include +#include +#include + #include -#include #include - #include #include // for the std::filesystem::path formatter @@ -35,32 +39,44 @@ namespace multipass::hyperv static constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; -struct ImageConversionException : public FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; +// Delegating constructor +HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) + : HCSVirtualMachineFactory(data_dir, + std::make_shared(), + std::make_shared(), + std::make_shared()) -struct ImageResizeException : public FormattedExceptionBase<> { - using FormattedExceptionBase<>::FormattedExceptionBase; -}; +} -HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) - : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) +HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir, + hcs_sptr_t hcs, + hcn_sptr_t hcn, + virtdisk_sptr_t virtdisk) + : BaseVirtualMachineFactory( + MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)), + hcs_sptr(hcs), + hcn_sptr(hcn), + virtdisk_sptr(virtdisk) { + const std::array api_ptrs = {hcs.get(), hcn.get(), virtdisk.get()}; + if (std::any_of(std::begin(api_ptrs), std::end(api_ptrs), [](const void* ptr) { return nullptr == ptr; })) + { + throw InvalidAPIPointerException{"One of the required API pointers is not set: {}.", fmt::join(api_ptrs, ",")}; + } } VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, VMStatusMonitor& monitor) { - auto hcs = std::make_unique(); - auto hcn = std::make_unique(); - auto virtdisk = std::make_unique(); + assert(hcs_sptr); + assert(hcn_sptr); + assert(virtdisk_sptr); - return std::make_unique(std::move(hcs), - std::move(hcn), - std::move(virtdisk), + return std::make_unique(hcs_sptr, + hcn_sptr, + virtdisk_sptr, kDefaultHyperVSwitchGUID, desc, monitor, @@ -126,12 +142,10 @@ VMImage HCSVirtualMachineFactory::prepare_source_image(const VMImage& source_ima void HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, const VirtualMachineDescription& desc) { - // FIXME: - virtdisk::VirtDiskWrapper wrap{}; - // Resize the instance image to the desired size + assert(virtdisk_sptr); const auto& [status, status_msg] = - wrap.resize_virtual_disk(instance_image.image_path.toStdString(), desc.disk_space.in_bytes()); + virtdisk_sptr->resize_virtual_disk(instance_image.image_path.toStdString(), desc.disk_space.in_bytes()); if (!status) { throw ImageResizeException{"Failed to resize VHDX file `{}`, virtdisk API error code `{}`", diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h index 5ce1b4a1b6..0f45aa801f 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -18,6 +18,8 @@ #ifndef MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_FACTORY_H #define MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_FACTORY_H +#include + #include namespace multipass::hyperv @@ -28,7 +30,9 @@ namespace multipass::hyperv */ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory { + explicit HCSVirtualMachineFactory(const Path& data_dir); + explicit HCSVirtualMachineFactory(const Path& data_dir, hcs_sptr_t hcs, hcn_sptr_t hcn, virtdisk_sptr_t virtdisk); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, @@ -60,6 +64,10 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory protected: std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; void remove_resources_for_impl(const std::string& name) override; + + hcs_sptr_t hcs_sptr{nullptr}; + hcn_sptr_t hcn_sptr{nullptr}; + virtdisk_sptr_t virtdisk_sptr{nullptr}; }; } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h b/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h new file mode 100644 index 0000000000..fbc98f8b37 --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h @@ -0,0 +1,46 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_WRAPPER_FWDECL_H +#define MULTIPASS_HYPERV_API_WRAPPER_FWDECL_H + +#include + +namespace multipass::hyperv +{ + +namespace hcs +{ +class HCSWrapperInterface; +} + +namespace hcn +{ +class HCNWrapperInterface; +} + +namespace virtdisk +{ +class VirtDiskWrapperInterface; +} + +using hcs_sptr_t = std::shared_ptr; +using hcn_sptr_t = std::shared_ptr; +using virtdisk_sptr_t = std::shared_ptr; +} // namespace multipass::hyperv + +#endif From e87ce812dcd1b954e15acab3f05bdc76b46bf8df Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 11 Mar 2025 17:44:03 +0300 Subject: [PATCH 17/76] [hyperv-api-vm|factory] switch to the log level fns --- .../hyperv_api/hcs_virtual_machine.cpp | 107 +++++++----------- 1 file changed, 44 insertions(+), 63 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index d741bfea97..6c469ddc58 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -72,19 +72,16 @@ inline auto replace_colon_with_dash(std::string& addr) */ auto resolve_ip_addresses(const std::string& hostname) { - mpl::log(lvl::trace, kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`", hostname); + mpl::trace(kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`", hostname); static auto wsa_context = [] { struct wsa_init_wrapper { wsa_init_wrapper() : wsa_data{}, wsa_init_success(WSAStartup(MAKEWORD(2, 2) == 0, &wsa_data)) { - mpl::log(lvl::debug, - kLogCategory, - "resolve_ip_addresses() -> initialized WSA, status `{}`", - wsa_init_success); + mpl::debug(kLogCategory, "resolve_ip_addresses() -> initialized WSA, status `{}`", wsa_init_success); if (!wsa_init_success) { - mpl::log(lvl::error, kLogCategory, "resolve_ip_addresses() > WSAStartup failed!"); + mpl::error(kLogCategory, "resolve_ip_addresses() > WSAStartup failed!"); } } ~wsa_init_wrapper() @@ -136,8 +133,7 @@ auto resolve_ip_addresses(const std::string& hostname) break; } - mpl::log( - lvl::error, + mpl::error( kLogCategory, "resolve_ip_addresses() -> anomaly: received {} bytes of IPv4 address data while expecting {}!", ptr->ai_addrlen, @@ -155,8 +151,7 @@ auto resolve_ip_addresses(const std::string& hostname) ipv6.push_back(addr); break; } - mpl::log( - lvl::error, + mpl::error( kLogCategory, "resolve_ip_addresses() -> anomaly: received {} bytes of IPv6 address data while expecting {}!", ptr->ai_addrlen, @@ -169,12 +164,11 @@ auto resolve_ip_addresses(const std::string& hostname) } } - mpl::log(lvl::trace, - kLogCategory, - "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", - hostname, - fmt::join(ipv4, ","), - fmt::join(ipv6, ",")); + mpl::trace(kLogCategory, + "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", + hostname, + fmt::join(ipv4, ","), + fmt::join(ipv6, ",")); return std::make_pair(ipv4, ipv6); } @@ -311,7 +305,7 @@ void HCSVirtualMachine::maybe_create_compute_system() void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) { - mpl::log(lvl::debug, kLogCategory, "set_state() -> VM `{}` HCS state `{}`", vm_name, compute_system_state); + mpl::debug(kLogCategory, "set_state() -> VM `{}` HCS state `{}`", vm_name, compute_system_state); const auto prev_state = state; switch (compute_system_state) @@ -336,18 +330,13 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) if (state != prev_state) { - mpl::log(lvl::info, - kLogCategory, - "set_state() > VM {} state changed from {} to {}", - vm_name, - prev_state, - state); + mpl::info(kLogCategory, "set_state() > VM {} state changed from {} to {}", vm_name, prev_state, state); } } void HCSVirtualMachine::start() { - mpl::log(lvl::debug, kLogCategory, "start() -> Starting VM `{}`, current state {}", vm_name, state); + mpl::debug(kLogCategory, "start() -> Starting VM `{}`, current state {}", vm_name, state); // Create the compute system, if not created yet. maybe_create_compute_system(); @@ -359,29 +348,29 @@ void HCSVirtualMachine::start() const auto& [status, status_msg] = [&] { // Fetch the latest state value. const auto hcs_state = fetch_state_from_api(); - mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` HCS state is `{}`", vm_name, hcs_state); + mpl::debug(kLogCategory, "start() -> VM `{}` HCS state is `{}`", vm_name, hcs_state); switch (hcs_state) { case hcs::ComputeSystemState::paused: { - mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in paused state, resuming", vm_name); + mpl::debug(kLogCategory, "start() -> VM `{}` is in paused state, resuming", vm_name); return hcs->resume_compute_system(vm_name); } case hcs::ComputeSystemState::created: [[fallthrough]]; default: { - mpl::log(lvl::debug, kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); + mpl::debug(kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); return hcs->start_compute_system(vm_name); } } }(); - mpl::log(lvl::debug, kLogCategory, "start() -> Start/resume VM `{}`, result `{}`", vm_name, status); + mpl::debug(kLogCategory, "start() -> Start/resume VM `{}`, result `{}`", vm_name, status); } void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { - mpl::log(lvl::debug, kLogCategory, "shutdown() -> Shutting down VM `{}`, current state {}", vm_name, state); + mpl::debug(kLogCategory, "shutdown() -> Shutting down VM `{}`, current state {}", vm_name, state); switch (shutdown_policy) { @@ -391,18 +380,14 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall, // some way to signal "vmwp.exe" for a graceful shutdown, sysrq via console // or other "direct" means to trigger the shutdown. - mpl::log(lvl::debug, - kLogCategory, - "shutdown() -> Requested powerdown, initiating graceful shutdown for `{}`", - vm_name); + mpl::debug(kLogCategory, "shutdown() -> Requested powerdown, initiating graceful shutdown for `{}`", vm_name); ssh_exec("sudo shutdown -h now"); break; case ShutdownPolicy::Halt: case ShutdownPolicy::Poweroff: - mpl::log(lvl::debug, - kLogCategory, - "shutdown() -> Requested halt/poweroff, initiating forceful shutdown for `{}`", - vm_name); + mpl::debug(kLogCategory, + "shutdown() -> Requested halt/poweroff, initiating forceful shutdown for `{}`", + vm_name); drop_ssh_session(); // These are non-graceful variants. Just terminate the system immediately. hcs->terminate_compute_system(vm_name); @@ -415,7 +400,7 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) void HCSVirtualMachine::suspend() { - mpl::log(lvl::debug, kLogCategory, "suspend() -> Suspending VM `{}`, current state {}", vm_name, state); + mpl::debug(kLogCategory, "suspend() -> Suspending VM `{}`, current state {}", vm_name, state); const auto& [status, status_msg] = hcs->pause_compute_system(vm_name); set_state(fetch_state_from_api()); update_state(); @@ -443,13 +428,13 @@ std::string HCSVirtualMachine::management_ipv4() const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); if (ipv4.empty()) { - mpl::log(lvl::error, kLogCategory, "management_ipv4() > failed to resolve `{}`", ssh_hostname({})); + mpl::error(kLogCategory, "management_ipv4() > failed to resolve `{}`", ssh_hostname({})); return "UNKNOWN"; } const auto result = *ipv4.begin(); - mpl::log(lvl::trace, kLogCategory, "management_ipv4() > IP address is `{}`", result); + mpl::trace(kLogCategory, "management_ipv4() > IP address is `{}`", result); // Prefer the first one return result; } @@ -483,50 +468,46 @@ hcs::ComputeSystemState HCSVirtualMachine::fetch_state_from_api() void HCSVirtualMachine::update_cpus(int num_cores) { - mpl::log(lvl::debug, kLogCategory, "update_cpus() -> called for VM `{}`, num_cores `{}`", vm_name, num_cores); + mpl::debug(kLogCategory, "update_cpus() -> called for VM `{}`, num_cores `{}`", vm_name, num_cores); throw std::runtime_error{"Not yet implemented"}; } void HCSVirtualMachine::resize_memory(const MemorySize& new_size) { - mpl::log(lvl::debug, - kLogCategory, - "resize_memory() -> called for VM `{}`, new_size `{}` MiB", - vm_name, - new_size.in_megabytes()); + mpl::debug(kLogCategory, + "resize_memory() -> called for VM `{}`, new_size `{}` MiB", + vm_name, + new_size.in_megabytes()); const auto& [status, status_msg] = hcs->resize_memory(vm_name, new_size.in_megabytes()); } void HCSVirtualMachine::resize_disk(const MemorySize& new_size) { - mpl::log(lvl::debug, - kLogCategory, - "resize_disk() -> called for VM `{}`, new_size `{}` MiB", - vm_name, - new_size.in_megabytes()); + mpl::debug(kLogCategory, + "resize_disk() -> called for VM `{}`, new_size `{}` MiB", + vm_name, + new_size.in_megabytes()); throw std::runtime_error{"Not yet implemented"}; } void HCSVirtualMachine::add_network_interface(int index, const std::string& default_mac_addr, const NetworkInterface& extra_interface) { - mpl::log(lvl::debug, - kLogCategory, - "add_network_interface() -> called for VM `{}`, index: {}, default_mac: {}, extra_interface: (mac: {}, " - "auto_mode: {}, id: {})" - "auto_mode: {}, ", - vm_name, - index, - default_mac_addr, - extra_interface.mac_address, - extra_interface.auto_mode, - extra_interface.id); + mpl::debug(kLogCategory, + "add_network_interface() -> called for VM `{}`, index: {}, default_mac: {}, extra_interface: (mac: {}, " + "mac_address: {}, id: {})", + vm_name, + index, + default_mac_addr, + extra_interface.mac_address, + extra_interface.auto_mode, + extra_interface.id); throw std::runtime_error{"Not yet implemented"}; } std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const std::string& target, const VMMount& mount) { - mpl::log(lvl::debug, kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); + mpl::debug(kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); throw std::runtime_error{"Not yet implemented"}; } From 45d4a4028c4fd7d850cfc3348def593a26680a02 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 12 Mar 2025 19:59:07 +0300 Subject: [PATCH 18/76] [hyperv-api-vm] Implement Plan9 shares Implement the host-side of the Plan9 shares. It needs an agent to be running on the guest system, which will be implemented later. Add support for the graceful shutdown using guest hv_utils linux kernel driver, if present. --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 161 ++++++++++++++++-- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 29 ++++ .../hyperv_hcs_create_compute_system_params.h | 15 ++ .../hcs/hyperv_hcs_plan9_share_params.h | 104 +++++++++++ .../hcs/hyperv_hcs_wrapper_interface.h | 5 + .../hyperv_api/hcs_plan9_mount_handler.cpp | 155 +++++++++++++++++ .../hyperv_api/hcs_plan9_mount_handler.h | 47 +++++ .../hyperv_api/hcs_virtual_machine.cpp | 35 ++-- .../hyperv_api/hyperv_api_wrapper_fwdecl.h | 6 +- tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 44 +++++ tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 109 ++++++++++-- 12 files changed, 665 insertions(+), 46 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h create mode 100644 src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp create mode 100644 src/platform/backends/hyperv_api/hcs_plan9_mount_handler.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 7213e47a24..4241879b43 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -45,6 +45,7 @@ if(WIN32) hcn/hyperv_hcn_api_wrapper.cpp hcs/hyperv_hcs_api_wrapper.cpp virtdisk/virtdisk_api_wrapper.cpp + hcs_plan9_mount_handler.cpp hcs_virtual_machine.cpp hcs_virtual_machine_factory.cpp ) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index fa9754e4fd..dde9d526af 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -92,18 +92,20 @@ OperationResult wait_for_operation_result(const HCSAPITable& api, timeout.count()); wchar_t* result_msg_out{nullptr}; - const auto result = api.WaitForOperationResult(op.get(), timeout.count(), &result_msg_out); + const auto hresult_code = ResultCode{api.WaitForOperationResult(op.get(), timeout.count(), &result_msg_out)}; UniqueHlocalString result_msg{result_msg_out, api.LocalFree}; - - if (result_msg) - { - // TODO: Convert from wstring to ascii and log this - // mpl::debug(kLogCategory, - // "wait_for_operation_result(...): ({}), result: {}, result_msg: {}", fmt::ptr(op.get()), - // result, result_msg); - return OperationResult{result, result_msg.get()}; - } - return OperationResult{result, L""}; + mpl::debug(kLogCategory, + "wait_for_operation_result(...) > finished ({}), result_code: {}", + fmt::ptr(op.get()), + hresult_code); + + const auto result = OperationResult{hresult_code, result_msg ? result_msg.get() : L""}; + // FIXME: Replace with unicode logging + fmt::print(L"{}{}{}", + result.status_msg.empty() ? L"" : L"Result document: ", + result.status_msg, + result.status_msg.empty() ? L"" : L"\n"); + return result; } // --------------------------------------------------------- @@ -276,6 +278,31 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam ; }(); + const auto plan9_shares = [&]() { + std::vector plan9_shares = {}; + + constexpr auto plan9_share_template = LR"( + {{ + "Name": "{0}", + "Path": "{1}", + "Port": {2}, + "AccessName": "{3}" + }} + )"; + + for (const auto& share : params.shares) + { + plan9_shares.push_back(fmt::format(plan9_share_template, + string_to_wstring(share.name), + share.host_path.wstring(), + share.port, + string_to_wstring(share.access_name), + fmt::underlying(share.flags))); + } + + return fmt::format(L"{}", fmt::join(plan9_shares, L", ")); + }(); + // Ideally, we should codegen from the schema // and use that. // https://raw.githubusercontent.com/MicrosoftDocs/Virtualization-Documentation/refs/heads/main/hyperv-samples/hcs-samples/JSON_files/HCS_Schema%5BWindows_10_SDK_version_1809%5D.json @@ -315,17 +342,35 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam }}, "Scsi": {{ {3} + }}, + "NetworkAdapters": {{ + {4} + }}, + "Plan9": {{ + "Shares": [ + {5} + ] }} + }}, + "Services": {{ + "Shutdown": {{}}, + "Heartbeat": {{}} }} }} }})"; + // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#HvSocketSystemConfig // Render the template const auto vm_settings = fmt::format(vm_settings_template, params.processor_count, params.memory_size_mb, string_to_wstring(params.name), - scsi_devices); + scsi_devices, + network_adapters, + plan9_shares); + + // FIXME: Replace this with wide-string logging API when it's available. + fmt::print(L"Rendered VM settings document: \n{}\n", vm_settings); HCS_SYSTEM system{nullptr}; auto operation = create_operation(api); @@ -363,7 +408,14 @@ OperationResult HCSWrapper::start_compute_system(const std::string& compute_syst OperationResult HCSWrapper::shutdown_compute_system(const std::string& compute_system_name) const { mpl::debug(kLogCategory, "shutdown_compute_system(...) > name: ({})", compute_system_name); - return perform_hcs_operation(api, api.ShutDownComputeSystem, compute_system_name, nullptr); + + static constexpr wchar_t c_shutdownOption[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; + + return perform_hcs_operation(api, api.ShutDownComputeSystem, compute_system_name, c_shutdownOption); } // --------------------------------------------------------- @@ -552,4 +604,87 @@ OperationResult HCSWrapper::get_compute_system_state(const std::string& compute_ return {result.code, L""}; } +// --------------------------------------------------------- + +OperationResult HCSWrapper::add_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const +{ + + mpl::debug(kLogCategory, "add_plan9_share(...) > name: ({}), params({})", compute_system_name, params); + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/plan9.go#L13 + // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#System_PropertyType + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/hcs/schema2/plan9_share.go#L12 + + // Settings: hcsschema.Plan9Share{ + // Name: name, + // AccessName: name, + // Path: path, + // Port: port, + // Flags: flags, + // AllowedFiles: allowed, // < this one is not supported in the base API + // }, + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/builder.go#L53 + constexpr auto add_plan9_share_template = LR"( + {{ + "ResourcePath": "VirtualMachine/Devices/Plan9/Shares", + "RequestType": "Add", + "Settings": {{ + "Name": "{0}", + "Path": "{1}", + "Port": {2}, + "AccessName": "{3}", + "Flags": 33 + }} + }})"; + + auto preferred{params.host_path}; + preferred.make_preferred(); + + (void)grant_vm_access(compute_system_name, params.host_path); + + const auto settings = fmt::format(add_plan9_share_template, + string_to_wstring(params.name), + // generic_* always uses / as separator, use it + // to normalize the path. HCS API does not like + // `\` for example. + preferred.generic_wstring(), + params.port, + string_to_wstring(params.access_name), + fmt::underlying(params.flags)); + fmt::print(L"{}", settings); + return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::remove_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const +{ + mpl::debug(kLogCategory, "remove_plan9_share(...) > name: ({}), params({})", compute_system_name, params); + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/plan9.go#L29 + + // Settings: hcsschema.Plan9Share{ + // Name: name, + // AccessName: name, + // Port: port, + // }, + + constexpr auto remove_plan9_share_template = LR"( + {{ + "ResourcePath": "VirtualMachine/Devices/Plan9/Shares", + "RequestType": "Remove", + "Settings": {{ + "Name": "{0}", + "AccessName": "{1}", + "Port": {2} + }} + }})"; + + const auto settings = fmt::format(remove_plan9_share_template, + string_to_wstring(params.name), + string_to_wstring(params.access_name), + params.port); + return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); +} + } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index 61d000f455..110019fcf7 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -230,6 +230,35 @@ struct HCSWrapper : public HCSWrapperInterface [[nodiscard]] OperationResult get_compute_system_state(const std::string& compute_system_name, ComputeSystemState& state_out) const override; + // --------------------------------------------------------- + + /** + * Add a Plan9 share to a running system + * + * @param [in] compute_system_name Target compute system's name + * @param [in] params Plan9 share details + * + * @return An object that evaluates to true on success, false otherwise. + * message() may contain details of failure when result is false. + */ + [[nodiscard]] OperationResult add_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const override; + + // --------------------------------------------------------- + + /** + * Remove a Plan9 share from a running system + * + * @param [in] compute_system_name Target compute system's name + * @param [in] params Plan9 share to remove. It's sufficient to fill the + * name, access_name and port. The rest are redundant for remove. + * + * @return An object that evaluates to true on success, false otherwise. + * message() may contain details of failure when result is false. + */ + [[nodiscard]] OperationResult remove_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const override; + private: const HCSAPITable api{}; }; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 365069f56d..fc3694f392 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -18,6 +18,9 @@ #ifndef MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H #define MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H +#include +#include + #include #include @@ -53,6 +56,18 @@ struct CreateComputeSystemParameters * Path to the Primary (boot) VHDX file */ std::string vhdx_path{}; + + /** + * List of endpoints that'll be added to the compute system + * by default at creation time. + */ + std::vector endpoints{}; + + /** + * List of Plan9 shares that'll be added to the compute system + * by default at creation time. + */ + std::vector shares{}; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h new file mode 100644 index 0000000000..79e91beb85 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -0,0 +1,104 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_PLAN9_SHARE_PARAMS_H +#define MULTIPASS_HYPERV_API_HCS_PLAN9_SHARE_PARAMS_H + +#include +#include +#include + +namespace multipass::hyperv::hcs +{ + +enum class Plan9ShareFlags : std::uint32_t +{ + none = 0, + read_only = 0x00000001, + linux_metadata = 0x00000004, + case_sensitive = 0x00000008 +}; + +/** + * Parameters for creating a Plan9 share. + */ +struct Plan9ShareParameters +{ + /** + * The default port number for Plan9. + * + * It's different from the official default port number + * since the host might want to run a Plan9 server itself. + */ + static inline constexpr std::uint16_t default_port{55035}; + + /** + * Unique name for the share + */ + std::string name{}; + + /** + * The name by which the guest operation system can access this share + * via the aname parameter in the Plan9 protocol. + */ + std::string access_name{}; + + /** + * Host directory to share + */ + std::filesystem::path host_path{}; + + /** + * Target path. + */ + std::uint16_t port{default_port}; + + /** + * ReadOnly 0x00000001 + * LinuxMetadata 0x00000004 + * CaseSensitive 0x00000008 + */ + Plan9ShareFlags flags{Plan9ShareFlags::none}; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcs::Plan9ShareParameters& params, FormatContext& ctx) const + { + return format_to(ctx.out(), + "Share name: ({}) | Access name: ({}) | Host path: ({}) | Port: ({}) ", + params.name, + params.access_name, + params.host_path, + params.port, + fmt::underlying(params.flags)); + } +}; + +#endif // MULTIPASS_HYPERV_API_HCS_ADD_9P_SHARE_PARAMS_H diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index 5fbba001b1..f6f56695cb 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,10 @@ struct HCSWrapperInterface const std::uint32_t new_core_count) const = 0; virtual OperationResult get_compute_system_state(const std::string& compute_system_name, ComputeSystemState& state_out) const = 0; + virtual OperationResult add_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const = 0; + virtual OperationResult remove_plan9_share(const std::string& compute_system_name, + const Plan9ShareParameters& params) const = 0; virtual ~HCSWrapperInterface() = default; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp new file mode 100644 index 0000000000..56fafb4304 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include +#include +#include + +#include + +namespace multipass::hyperv::hcs +{ + +namespace mpu = utils; + +constexpr auto kLogCategory = "hcs-plan9-mount-handler"; + +Plan9MountHandler::Plan9MountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + VMMount mount_spec, + const std::string& target, + hcs_sptr_t hcs_w) + : MountHandler(vm, ssh_key_provider, mount_spec, target), hcs{hcs_w} +{ + // No need to do anything special. + if (nullptr == hcs) + { + throw std::invalid_argument{"HCS API wrapper object cannot be null."}; + } + if (nullptr == vm) + { + throw std::invalid_argument{"VM pointer cannot be null."}; + } +} + +Plan9MountHandler::~Plan9MountHandler() = default; + +void Plan9MountHandler::activate_impl(ServerVariant server, std::chrono::milliseconds timeout) +{ + const auto params = [this] { + hcs::Plan9ShareParameters params{}; + params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + params.name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + params.host_path = mount_spec.get_source_path(); + return params; + }(); + + const auto result = hcs->add_plan9_share(vm->vm_name, params); + + if (!result) + { + throw std::runtime_error{"Failed to create a Plan9 share for the mount"}; + } + + try + { + // The host side 9P share setup is done. Let's handle the guest side. + SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *ssh_key_provider}; + + // Split the path in existing and missing parts + // We need to create the part of the path which does not still exist, and set then the correct ownership. + if (const auto& [leading, missing] = mpu::get_path_split(session, target); missing != ".") + { + const auto default_uid = std::stoi(MP_UTILS.run_in_ssh_session(session, "id -u")); + mpl::log(mpl::Level::debug, + kLogCategory, + fmt::format("{}:{} {}(): `id -u` = {}", __FILE__, __LINE__, __FUNCTION__, default_uid)); + const auto default_gid = std::stoi(MP_UTILS.run_in_ssh_session(session, "id -g")); + mpl::log(mpl::Level::debug, + kLogCategory, + fmt::format("{}:{} {}(): `id -g` = {}", __FILE__, __LINE__, __FUNCTION__, default_gid)); + + mpu::make_target_dir(session, leading, missing); + mpu::set_owner_for(session, leading, missing, default_uid, default_gid); + } + + // fmt::format("sudo mount -t 9p {} {} -o trans=virtio,version=9p2000.L,msize=536870912", tag, target)); + constexpr std::string_view mount_command_fmtstr = + "sudo mount -t 9p -o trans=virtio,version=9p2000.L,port={} {} {}"; + + const auto mount_command = fmt::format(mount_command_fmtstr, params.port, params.access_name, target); + + auto mount_command_result = session.exec(mount_command); + + if (mount_command_result.exit_code() == 0) + { + mpl::info(kLogCategory, "Successfully mounted 9P share `{}` to VM `{}`", params, vm->vm_name); + } + else + { + mpl::error(kLogCategory, + "stdout: {} stderr: {}", + mount_command_result.read_std_output(), + mount_command_result.read_std_error()); + throw std::runtime_error{"Failed to mount the Plan9 share"}; + } + } + catch (...) + { + if (!hcs->remove_plan9_share(vm->vm_name, params)) + { + // TODO: Warn here + } + } +} +void Plan9MountHandler::deactivate_impl(bool force) +{ + SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *ssh_key_provider}; + constexpr std::string_view umount_command_fmtstr = "mountpoint -q {0}; then sudo umount {0}; else true; fi"; + const auto umount_command = fmt::format(umount_command_fmtstr, target); + + if (!(session.exec(umount_command).exit_code() == 0)) + { + // TODO: Include output? + mpl::warn(kLogCategory, "Plan9 share unmount failed."); + + if (!force) + { + return; + } + } + + const auto params = [this] { + hcs::Plan9ShareParameters params{}; + params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + params.name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + params.host_path = mount_spec.get_source_path(); + return params; + }(); + + if (!hcs->remove_plan9_share(vm->vm_name, params)) + { + mpl::warn(kLogCategory, "Plan9 share removal failed."); + } +} + +// No need for custom active logic. + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.h b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.h new file mode 100644 index 0000000000..cfbe967d90 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.h @@ -0,0 +1,47 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_HCS_PLAN9_MOUNT_HANDLER_H +#define MULTIPASS_HYPERV_HCS_PLAN9_MOUNT_HANDLER_H + +#include +#include + +namespace multipass::hyperv::hcs +{ + +class Plan9MountHandler : public MountHandler +{ +public: + Plan9MountHandler(VirtualMachine* vm, + const multipass::SSHKeyProvider* ssh_key_provider, + VMMount mount_spec, + const std::string& target, + hcs_sptr_t hcs_w); + + ~Plan9MountHandler() override; + +private: + void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; + void deactivate_impl(bool force) override; + + hcs_sptr_t hcs{nullptr}; +}; + +} // namespace multipass::hyperv::hcs + +#endif diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 6c469ddc58..16a9930e88 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -278,6 +279,14 @@ void HCSVirtualMachine::maybe_create_compute_system() // endpoint_params.target_compute_system_name = ccs_params.name; // ccs_params.endpoints.push_back(endpoint_params); // } + + { + hcs::Plan9ShareParameters share{}; + share.access_name = "test"; + share.name = "test"; + share.host_path = "C:/"; + ccs_params.shares.push_back(share); + } return ccs_params; }(); @@ -328,10 +337,10 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) break; } - if (state != prev_state) - { - mpl::info(kLogCategory, "set_state() > VM {} state changed from {} to {}", vm_name, prev_state, state); - } + if (state == prev_state) + return; + + mpl::info(kLogCategory, "set_state() > VM {} state changed from {} to {}", vm_name, prev_state, state); } void HCSVirtualMachine::start() @@ -375,13 +384,13 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) switch (shutdown_policy) { case ShutdownPolicy::Powerdown: - // We have to rely on SSH for now since we don't have the means to - // run a guest action from host natively. - // FIXME: Find a way to trigger ACPI shutdown, host-to-guest syscall, - // some way to signal "vmwp.exe" for a graceful shutdown, sysrq via console - // or other "direct" means to trigger the shutdown. mpl::debug(kLogCategory, "shutdown() -> Requested powerdown, initiating graceful shutdown for `{}`", vm_name); - ssh_exec("sudo shutdown -h now"); + + // If the guest has integration modules enabled, we can use graceful shutdown. + if(!hcs->shutdown_compute_system(vm_name)){ + // Fall back to SSH shutdown. + ssh_exec("sudo shutdown -h now"); + } break; case ShutdownPolicy::Halt: case ShutdownPolicy::Poweroff: @@ -507,8 +516,10 @@ void HCSVirtualMachine::add_network_interface(int index, std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const std::string& target, const VMMount& mount) { - mpl::debug(kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name); - throw std::runtime_error{"Not yet implemented"}; + mpl::debug(kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name, target); + + throw NotImplementedOnThisBackendException{"Plan9 mounts require an agent running on guest, which is not implemented yet."}; + // return std::make_unique(this, &key_provider, mount, target, hcs); } } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h b/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h index fbc98f8b37..37a8b83e06 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h +++ b/src/platform/backends/hyperv_api/hyperv_api_wrapper_fwdecl.h @@ -38,9 +38,9 @@ namespace virtdisk class VirtDiskWrapperInterface; } -using hcs_sptr_t = std::shared_ptr; -using hcn_sptr_t = std::shared_ptr; -using virtdisk_sptr_t = std::shared_ptr; +using hcs_sptr_t = std::shared_ptr; +using hcn_sptr_t = std::shared_ptr; +using virtdisk_sptr_t = std::shared_ptr; } // namespace multipass::hyperv #endif diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 8a38e64692..a2176cf3cd 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -171,4 +171,48 @@ TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) ASSERT_FALSE(d_result.status_msg.empty()); } +TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) +{ + + uut_t uut{}; + + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = "test"; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.cloudinit_iso_path = ""; + params.vhdx_path = ""; + + const auto c_result = uut.create_compute_system(params); + + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); + + const auto s_result = uut.start_compute_system(params.name); + ASSERT_TRUE(s_result); + ASSERT_TRUE(s_result.status_msg.empty()); + + const auto p_result = uut.get_compute_system_properties(params.name); + EXPECT_TRUE(p_result); + std::wprintf(L"%s\n", p_result.status_msg.c_str()); + + hyperv::hcs::Plan9ShareParameters share{}; + share.access_name = "test"; + share.name = "test"; + share.host_path = "C://"; + + const auto sh_a_result = uut.add_plan9_share(params.name, share); + EXPECT_TRUE(sh_a_result); + std::wprintf(L"%s\n", sh_a_result.status_msg.c_str()); + + const auto sh_r_result = uut.remove_plan9_share(params.name, share); + EXPECT_TRUE(sh_r_result); + std::wprintf(L"%s\n", sh_r_result.status_msg.c_str()); + + const auto d_result = uut.terminate_compute_system(params.name); + ASSERT_TRUE(d_result); + std::wprintf(L"%s\n", d_result.status_msg.c_str()); + ASSERT_FALSE(d_result.status_msg.empty()); +} + } // namespace multipass::test diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 3150509d39..8299df1932 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -238,7 +238,15 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) } } }, - "NetworkAdapters": {} + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -305,7 +313,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " "Memory size: (16384 MiB) | cloud-init ISO path: (cloudinit iso path) | VHDX path: (virtual disk path)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -396,7 +405,15 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) } } }, - "NetworkAdapters": {} + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -463,7 +480,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " "Memory size: (16384 MiB) | cloud-init ISO path: () | VHDX path: (virtual disk path)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -554,7 +572,15 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) } } }, - "NetworkAdapters": {} + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -621,7 +647,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " "Memory size: (16384 MiB) | cloud-init ISO path: (cloudinit iso path) | VHDX path: ()"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -701,8 +728,16 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) "NamedPipe": "\\\\.\\pipe\\test_vm" } }, - "Scsi": { + "Scsi": {}, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -769,7 +804,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " "Memory size: (16384 MiB) | cloud-init ISO path: () | VHDX path: ()"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -914,8 +950,16 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) } } }, - "NetworkAdapters": {} - } + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } } })"; @@ -1058,8 +1102,16 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) } } }, - "NetworkAdapters": {} - } + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } } })"; @@ -1122,7 +1174,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -1412,7 +1465,8 @@ void HyperVHCSAPI_UnitTests::generic_operation_happy_path(ApiFnT& target_api_fun logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_hcs_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -1731,7 +1785,8 @@ void HyperVHCSAPI_UnitTests::generic_operation_wait_for_operation_fail(ApiFnT& t logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_hcs_operation(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -1835,6 +1890,11 @@ TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_wait_for_operation_result_fa TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_happy_path) { + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; generic_operation_happy_path( mock_api_table.ShutDownComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { @@ -1844,7 +1904,8 @@ TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_happy_path) [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { ASSERT_EQ(mock_compute_system_object, computeSystem); ASSERT_EQ(mock_operation_object, operation); - ASSERT_EQ(options, nullptr); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); }); } @@ -1877,6 +1938,11 @@ TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_create_operation_fail) TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_fail) { + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; generic_operation_fail( mock_api_table.ShutDownComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { @@ -1886,7 +1952,8 @@ TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_fail) [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { ASSERT_EQ(mock_compute_system_object, computeSystem); ASSERT_EQ(mock_operation_object, operation); - ASSERT_EQ(options, nullptr); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); }); } @@ -1894,6 +1961,11 @@ TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_fail) TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_wait_for_operation_result_fail) { + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; generic_operation_wait_for_operation_fail( mock_api_table.ShutDownComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { @@ -1903,7 +1975,8 @@ TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_wait_for_operation_result [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { ASSERT_EQ(mock_compute_system_object, computeSystem); ASSERT_EQ(mock_operation_object, operation); - ASSERT_EQ(options, nullptr); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); }); } From 6891aea06f898229b84dc1fb5e814667623511e0 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 24 Mar 2025 18:34:48 +0300 Subject: [PATCH 19/76] [hyperv-api-factory] implement remove_resources_for_impl There are nothing to remove since everything is neatly packed into the instance folder. Removal of the instance folder is already taken care of by the base class, so only thing left is to ensure that the compute system is terminated. Set log level of open_compute_system failure and operation_failure to debug() instead of error() since these functions may fail in normal operation workflow (i.e. check if a VM exists). --- .../backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 4 ++-- .../backends/hyperv_api/hcs_virtual_machine_factory.cpp | 9 ++++++++- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index dde9d526af..069b90c8cc 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -131,7 +131,7 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri if (!result) { - mpl::error(kLogCategory, + mpl::debug(kLogCategory, "open_host_compute_system(...) > failed to open ({}), result code: ({})", name, result); @@ -174,7 +174,7 @@ OperationResult perform_hcs_operation(const HCSAPITable& api, if (nullptr == system) { - mpl::error(kLogCategory, + mpl::debug(kLogCategory, "perform_hcs_operation(...) > HcsOpenComputeSystem failed! {}", target_hcs_system_name); return OperationResult{E_INVALIDARG, L"HcsOpenComputeSystem failed!"}; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 03817aa5bc..5dfe60a6a6 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -86,7 +86,14 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine(const Virt void HCSVirtualMachineFactory::remove_resources_for_impl(const std::string& name) { - throw std::runtime_error{"Not implemented yet."}; + mpl::debug(kLogCategory, "remove_resources_for_impl() -> VM: {}", name); + // Everything for the VM is neatly packed into the VM folder, so it's enough to ensure that + // the VM is stopped. The base class will take care of the nuking the VM folder. + const auto& [status, status_msg] = hcs_sptr->terminate_compute_system(name); + if (status) + { + mpl::warn(kLogCategory, "remove_resources_for_impl() -> Host compute system {} was still alive.", name); + } } VMImage HCSVirtualMachineFactory::prepare_source_image(const VMImage& source_image) diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 8299df1932..832afbc5fc 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -1521,9 +1521,9 @@ void HyperVHCSAPI_UnitTests::generic_operation_hcs_open_fail(ApiFnT& target_api_ logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...) > name: (test_vm)"); logger_scope.mock_logger->expect_log( - mpl::Level::error, + mpl::Level::debug, "open_host_compute_system(...) > failed to open (test_vm), result code: (0x80004003)"); - logger_scope.mock_logger->expect_log(mpl::Level::error, + logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_hcs_operation(...) > HcsOpenComputeSystem failed!"); } From 3be80dec411b3a3374433f887cb97cbae466ff24 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 24 Mar 2025 21:17:33 +0300 Subject: [PATCH 20/76] [hyperv-api-factory] implement clone vm functionality simple implementation, vhdx clone only. - [hcs_api_wrapper] normalize file paths for api calls - [virtdisk_wrapper] enable clone functionality for CreateVirtualDisk --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 4 +- .../hyperv_hcs_create_compute_system_params.h | 5 +- .../hyperv_api/hcs_virtual_machine.cpp | 10 +- .../hcs_virtual_machine_factory.cpp | 49 +++++++ .../hyperv_api/hcs_virtual_machine_factory.h | 7 + .../hyperv_api/hyperv_api_operation_result.h | 2 +- .../virtdisk/virtdisk_api_wrapper.cpp | 50 ++++++- .../virtdisk_create_virtual_disk_params.h | 2 + tests/hyperv_api/test_ut_hyperv_virtdisk.cpp | 134 ++++++++++++++++++ 9 files changed, 245 insertions(+), 18 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 069b90c8cc..b35ddda86d 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -242,7 +242,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam scsi_nodes.push_back(fmt::format(scsi_device_template, L"cloud-init iso file", L"Iso", - string_to_wstring(params.cloudinit_iso_path), + params.cloudinit_iso_path.generic_wstring(), true)); } @@ -251,7 +251,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam scsi_nodes.push_back(fmt::format(scsi_device_template, L"Primary disk", L"VirtualDisk", - string_to_wstring(params.vhdx_path), + params.vhdx_path.generic_wstring(), false)); } diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index fc3694f392..664c8c6c57 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -22,6 +22,7 @@ #include #include +#include #include namespace multipass::hyperv::hcs @@ -50,12 +51,12 @@ struct CreateComputeSystemParameters /** * Path to the cloud-init ISO file */ - std::string cloudinit_iso_path{}; + std::filesystem::path cloudinit_iso_path{}; /** * Path to the Primary (boot) VHDX file */ - std::string vhdx_path{}; + std::filesystem::path vhdx_path{}; /** * List of endpoints that'll be added to the compute system diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 16a9930e88..2131216ccd 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -280,21 +280,13 @@ void HCSVirtualMachine::maybe_create_compute_system() // ccs_params.endpoints.push_back(endpoint_params); // } - { - hcs::Plan9ShareParameters share{}; - share.access_name = "test"; - share.name = "test"; - share.host_path = "C:/"; - ccs_params.shares.push_back(share); - } return ccs_params; }(); if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) { - fmt::print(L"Create compute system failed: {}", create_result.status_msg); - throw CreateComputeSystemException{"create_compute_system failed with {}", create_result}; + throw CreateComputeSystemException{"create_compute_system failed with {}", create_result.code}; } // Grant access to the VHDX and the cloud-init ISO files. diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 5dfe60a6a6..6fec2c5326 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -25,6 +25,7 @@ #include #include +#include #include @@ -164,8 +165,56 @@ void HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_im std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) { (void)intf; + // No-op. The implementation uses the default Hyper-V switch. return {}; } +VirtualMachine::UPtr HCSVirtualMachineFactory::clone_vm_impl(const std::string& source_vm_name, + const multipass::VMSpecs& src_vm_specs, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider) +{ + + const fs::path src_vm_instance_dir{get_instance_directory(source_vm_name).toStdWString()}; + + if (!fs::exists(src_vm_instance_dir)) + { + throw std::runtime_error{"Source VM instance directory is missing!"}; + } + + std::optional src_vm_vhdx{std::nullopt}; + + for (const auto& entry : fs::directory_iterator(src_vm_instance_dir)) + { + const auto& extension = entry.path().extension(); + if (extension == ".vhdx") + { + src_vm_vhdx = entry.path(); + break; + } + } + + if (!src_vm_vhdx.has_value()) + { + throw std::runtime_error{"Could not locate source VM's vhdx file!"}; + } + + // Copy the VHDX file. + virtdisk::CreateVirtualDiskParameters clone_vhdx_params{}; + clone_vhdx_params.source = src_vm_vhdx.value(); + clone_vhdx_params.path = desc.image.image_path.toStdString(); + clone_vhdx_params.size_in_bytes = 0; // use source + + const auto& [status, msg] = virtdisk_sptr->create_virtual_disk(clone_vhdx_params); + + if (!status) + { + throw std::runtime_error{"VHDX clone failed."}; + } + + return create_virtual_machine(desc, key_provider, monitor); +} + } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h index 0f45aa801f..f8493d518d 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -65,6 +65,13 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; void remove_resources_for_impl(const std::string& name) override; +private: + VirtualMachine::UPtr clone_vm_impl(const std::string& source_vm_name, + const multipass::VMSpecs& src_vm_specs, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider) override; + hcs_sptr_t hcs_sptr{nullptr}; hcn_sptr_t hcn_sptr{nullptr}; virtdisk_sptr_t virtdisk_sptr{nullptr}; diff --git a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h index 0a906df477..75343c057a 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h +++ b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h @@ -123,7 +123,7 @@ struct fmt::formatter template auto format(const multipass::hyperv::OperationResult& opr, FormatContext& ctx) const { - return format_to(ctx.out(), "{:#x}", opr.code); + return format_to(ctx.out(), "{}", opr.code); } }; diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index b419919fd8..678e59fdf0 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -32,6 +32,12 @@ namespace multipass::hyperv::virtdisk namespace { +auto normalize_path(std::filesystem::path p) +{ + p.make_preferred(); + return p; +} + using UniqueHandle = std::unique_ptr, decltype(VirtDiskAPITable::CloseHandle)>; namespace mpl = logging; @@ -93,6 +99,8 @@ VirtDiskWrapper::VirtDiskWrapper(const VirtDiskAPITable& api_table) : api{api_ta OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskParameters& params) const { mpl::debug(kLogCategory, "create_virtual_disk(...) > params: {}", params); + + const auto target_path_normalized = normalize_path(params.path).wstring(); // // https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Hyper-V/Storage/cpp/CreateVirtualDisk.cpp // @@ -110,6 +118,43 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara parameters.Version2 = {}; parameters.Version2.MaximumSize = params.size_in_bytes; + // Tell virtdisk to copy the data from source when it's specified. + std::wstring source_path_normalized{}; + if (params.source.has_value()) + { + source_path_normalized = normalize_path(params.source.value()).wstring(); + parameters.Version2.SourcePath = source_path_normalized.c_str(); + + VirtualDiskInfo src_disk_info{}; + const auto result = get_virtual_disk_info(source_path_normalized, src_disk_info); + mpl::debug(kLogCategory, "create_virtual_disk(...) > source disk info fetch result `{}`", result); + + if (src_disk_info.virtual_storage_type) + { + if (src_disk_info.virtual_storage_type == "vhd") + { + parameters.Version2.SourceVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD; + } + else if (src_disk_info.virtual_storage_type == "vhdx") + { + parameters.Version2.SourceVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; + } + else if (src_disk_info.virtual_storage_type == "unknown") + { + throw std::runtime_error{"Unable to determine the source disk type."}; + } + else + { + throw std::runtime_error{"Unsupported source disk type for clone operation"}; + } + } + + mpl::debug(kLogCategory, + "create_virtual_disk(...) > cloning `{}` to `{}`", + std::filesystem::path{source_path_normalized}, + std::filesystem::path{target_path_normalized}); + } + // // Internal size of the virtual disk object blocks, in bytes. // For VHDX this must be a multiple of 1 MB between 1 and 256 MB. @@ -123,13 +168,11 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara parameters.Version2.BlockSizeInBytes = 524288; // 512 KiB } - const auto path_w = params.path.wstring(); - HANDLE result_handle{nullptr}; const auto result = api.CreateVirtualDisk(&type, // [in] PCWSTR Path - path_w.c_str(), + target_path_normalized.c_str(), // [in] VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, VIRTUAL_DISK_ACCESS_NONE, // [in, optional] PSECURITY_DESCRIPTOR SecurityDescriptor, @@ -279,7 +322,6 @@ OperationResult VirtDiskWrapper::get_virtual_disk_info(const std::filesystem::pa break; case ProviderSubtype::dynamic: vdinfo.provider_subtype = "dynamic"; - break; case ProviderSubtype::differencing: vdinfo.provider_subtype = "differencing"; diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h index 953157184c..33dc1c4626 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h @@ -19,6 +19,7 @@ #define MULTIPASS_HYPERV_API_VIRTDISK_CREATE_VIRTUAL_DISK_PARAMETERS_H #include +#include #include @@ -32,6 +33,7 @@ struct CreateVirtualDiskParameters { std::uint64_t size_in_bytes{}; std::filesystem::path path{}; + std::optional source{}; }; } // namespace multipass::hyperv::virtdisk diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index 0365eb6637..7c1ec13199 100644 --- a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -227,6 +227,140 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) // --------------------------------------------------------- +TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) +{ + /****************************************************** + * Override the default mock functions. + ******************************************************/ + ::testing::MockFunction mock_create_virtual_disk; + ::testing::MockFunction mock_open_virtual_disk; + ::testing::MockFunction mock_get_virtual_disk_information; + ::testing::MockFunction mock_close_handle; + + mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); + mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); + mock_api_table.GetVirtualDiskInformation = mock_get_virtual_disk_information.AsStdFunction(); + mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + EXPECT_CALL(mock_create_virtual_disk, Call) + .WillOnce(DoAll( + [](PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + PSECURITY_DESCRIPTOR SecurityDescriptor, + CREATE_VIRTUAL_DISK_FLAG Flags, + ULONG ProviderSpecificFlags, + PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped, + PHANDLE Handle + + ) { + ASSERT_NE(nullptr, VirtualStorageType); + ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); + ASSERT_NE(nullptr, Path); + ASSERT_STREQ(Path, L"test.vhdx"); + + ASSERT_EQ(VirtualDiskAccessMask, VIRTUAL_DISK_ACCESS_NONE); + ASSERT_EQ(nullptr, SecurityDescriptor); + ASSERT_EQ(CREATE_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_EQ(0, ProviderSpecificFlags); + ASSERT_NE(nullptr, Parameters); + ASSERT_EQ(Parameters->Version, CREATE_VIRTUAL_DISK_VERSION_2); + ASSERT_EQ(Parameters->Version2.MaximumSize, 0); + ASSERT_EQ(Parameters->Version2.BlockSizeInBytes, 1048576); + ASSERT_STREQ(Parameters->Version2.SourcePath, L"source.vhdx"); + ASSERT_EQ(Parameters->Version2.SourceVirtualStorageType.DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); + ASSERT_EQ(Parameters->Version2.SourceVirtualStorageType.VendorId, + VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); + + ASSERT_EQ(nullptr, Overlapped); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_open_virtual_disk, Call) + .WillOnce(DoAll( + [](PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) { + ASSERT_NE(nullptr, VirtualStorageType); + ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); + ASSERT_NE(nullptr, Path); + ASSERT_STREQ(Path, L"source.vhdx"); + ASSERT_EQ(VIRTUAL_DISK_ACCESS_ALL, VirtualDiskAccessMask); + ASSERT_EQ(OPEN_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_EQ(nullptr, Parameters); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + // The API will be called for several times. + EXPECT_CALL(mock_get_virtual_disk_information, Call) + .WillRepeatedly(DoAll( + [](HANDLE VirtualDiskHandle, + PULONG VirtualDiskInfoSize, + PGET_VIRTUAL_DISK_INFO VirtualDiskInfo, + PULONG SizeUsed) { + ASSERT_EQ(mock_handle_object, VirtualDiskHandle); + ASSERT_NE(nullptr, VirtualDiskInfoSize); + ASSERT_EQ(sizeof(GET_VIRTUAL_DISK_INFO), *VirtualDiskInfoSize); + ASSERT_NE(nullptr, VirtualDiskInfo); + ASSERT_EQ(nullptr, SizeUsed); + VirtualDiskInfo->VirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; + VirtualDiskInfo->VirtualStorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN; + VirtualDiskInfo->SmallestSafeVirtualSize = 123456; + VirtualDiskInfo->Size.VirtualSize = 1111111; + VirtualDiskInfo->Size.BlockSize = 2222222; + VirtualDiskInfo->Size.PhysicalSize = 3333333; + VirtualDiskInfo->Size.SectorSize = 4444444; + VirtualDiskInfo->ProviderSubtype = 3; // dynamic + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).Times(2).WillRepeatedly(Return(true)); + + logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); + logger_scope.mock_logger->expect_log( + mpl::Level::debug, + "create_virtual_disk(...) > params: Size (in bytes): (0) | Path: (test.vhdx)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: source.vhdx"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_virtual_disk_info(...) > vhdx_path: source.vhdx"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, + "create_virtual_disk(...) > source disk info fetch result"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_virtual_disk(...) > cloning"); + } + + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.source = "source.vhdx"; + params.path = "test.vhdx"; + params.size_in_bytes = 0; + + { + uut_t uut{mock_api_table}; + const auto& [status, status_msg] = uut.create_virtual_disk(params); + EXPECT_TRUE(status); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) { /****************************************************** From f681e99b6fd73c01d9b190b6e1a7f303c98d9c2c Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 27 Mar 2025 01:19:40 +0300 Subject: [PATCH 21/76] [hyperv-api-vm] return status of maybe_create_compute_system --- .../hyperv_api/hcs_virtual_machine.cpp | 178 ++++++++++-------- .../backends/hyperv_api/hcs_virtual_machine.h | 9 +- 2 files changed, 104 insertions(+), 83 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 2131216ccd..5981d5e0f8 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -204,104 +204,116 @@ HCSVirtualMachine::HCSVirtualMachine(hcs_sptr_t hcs_w, } } - // Craete - maybe_create_compute_system(); + const auto created_from_scratch = maybe_create_compute_system(); + const auto state = fetch_state_from_api(); + + mpl::debug(kLogCategory, + "HCSVirtualMachine::HCSVirtualMachine() > `{}`, created_from_scratch: {}, state: {}", + vm_name, + created_from_scratch, + state); + // Reflect compute system's state - set_state(fetch_state_from_api()); + set_state(state); update_state(); } -void HCSVirtualMachine::maybe_create_compute_system() +bool HCSVirtualMachine::maybe_create_compute_system() { hcs::ComputeSystemState cs_state{hcs::ComputeSystemState::unknown}; // Check if the VM already exist const auto result = hcs->get_compute_system_state(vm_name, cs_state); - if (E_INVALIDARG == static_cast(result.code)) + if (!(E_INVALIDARG == static_cast(result.code))) { - // FIXME: Handle suspend state? - - const auto endpoint_params = [this]() { - std::vector endpoint_params{}; - endpoint_params.emplace_back( - hcn::CreateEndpointParameters{primary_network_guid, mac2uuid(description.default_mac_address)}); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); - // } - return endpoint_params; - }(); - - for (const auto& endpoint : endpoint_params) + // Target compute system already exist. + return false; + } + + // FIXME: Handle suspend state? + + const auto endpoint_params = [this]() { + std::vector endpoint_params{}; + endpoint_params.emplace_back( + hcn::CreateEndpointParameters{primary_network_guid, mac2uuid(description.default_mac_address)}); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); + // } + return endpoint_params; + }(); + + for (const auto& endpoint : endpoint_params) + { + // There might be remnants from an old VM, remove the endpoint if exist before + // creating it again. + if (hcn->delete_endpoint(endpoint.endpoint_guid)) { - // There might be remnants from an old VM, remove the endpoint if exist before - // creating it again. - if (hcn->delete_endpoint(endpoint.endpoint_guid)) - { - // TODO: log - } - if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) - { - throw CreateEndpointException{"create_endpoint failed with {}", status}; - } + mpl::debug(kLogCategory, + "The endpoint {} was already present for the VM {}, removed it.", + endpoint.endpoint_guid, + vm_name); } - - // E_INVALIDARG means there's no such VM. - // Create the VM from scratch. - const auto ccs_params = [this, &endpoint_params]() { - hcs::CreateComputeSystemParameters ccs_params{}; - ccs_params.name = description.vm_name; - ccs_params.memory_size_mb = description.mem_size.in_megabytes(); - ccs_params.processor_count = description.num_cores; - ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); - ccs_params.vhdx_path = description.image.image_path.toStdString(); - - hcs::AddEndpointParameters default_endpoint_params{}; - default_endpoint_params.nic_mac_address = description.default_mac_address; - // Hyper-V API does not like colons. Ensure that the MAC is separated - // with dash instead of colon. - replace_colon_with_dash(default_endpoint_params.nic_mac_address); - // make the UUID deterministic so we can query the endpoint with a MAC address - // if needed. - default_endpoint_params.endpoint_guid = mac2uuid(description.default_mac_address); - default_endpoint_params.target_compute_system_name = ccs_params.name; - ccs_params.endpoints.push_back(default_endpoint_params); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // hcs::AddEndpointParameters endpoint_params{}; - // endpoint_params.nic_mac_address = v.mac_address; - // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); - // endpoint_params.target_compute_system_name = ccs_params.name; - // ccs_params.endpoints.push_back(endpoint_params); - // } - - return ccs_params; - }(); - - if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) { - fmt::print(L"Create compute system failed: {}", create_result.status_msg); - throw CreateComputeSystemException{"create_compute_system failed with {}", create_result.code}; + throw CreateEndpointException{"create_endpoint failed with {}", status}; } + } - // Grant access to the VHDX and the cloud-init ISO files. - const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; + // E_INVALIDARG means there's no such VM. + // Create the VM from scratch. + const auto ccs_params = [this, &endpoint_params]() { + hcs::CreateComputeSystemParameters ccs_params{}; + ccs_params.name = description.vm_name; + ccs_params.memory_size_mb = description.mem_size.in_megabytes(); + ccs_params.processor_count = description.num_cores; + ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); + ccs_params.vhdx_path = description.image.image_path.toStdString(); + + hcs::AddEndpointParameters default_endpoint_params{}; + default_endpoint_params.nic_mac_address = description.default_mac_address; + // Hyper-V API does not like colons. Ensure that the MAC is separated + // with dash instead of colon. + replace_colon_with_dash(default_endpoint_params.nic_mac_address); + // make the UUID deterministic so we can query the endpoint with a MAC address + // if needed. + default_endpoint_params.endpoint_guid = mac2uuid(description.default_mac_address); + default_endpoint_params.target_compute_system_name = ccs_params.name; + ccs_params.endpoints.push_back(default_endpoint_params); + + // TODO: Figure out what to do with the "extra interfaces" + // for (const auto& v : desc.extra_interfaces) + // { + // hcs::AddEndpointParameters endpoint_params{}; + // endpoint_params.nic_mac_address = v.mac_address; + // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); + // endpoint_params.target_compute_system_name = ccs_params.name; + // ccs_params.endpoints.push_back(endpoint_params); + // } + + return ccs_params; + }(); + + if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + { + fmt::print(L"Create compute system failed: {}", create_result.status_msg); + throw CreateComputeSystemException{"create_compute_system failed with {}", create_result.code}; + } - for (const auto& path : grant_paths) + // Grant access to the VHDX and the cloud-init ISO files. + const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; + + for (const auto& path : grant_paths) + { + if (!hcs->grant_vm_access(ccs_params.name, path)) { - if (!hcs->grant_vm_access(ccs_params.name, path)) - { - throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", - ccs_params.name, - path}; - } + throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", ccs_params.name, path}; } } + return true; } void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) @@ -377,9 +389,10 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { case ShutdownPolicy::Powerdown: mpl::debug(kLogCategory, "shutdown() -> Requested powerdown, initiating graceful shutdown for `{}`", vm_name); - + // If the guest has integration modules enabled, we can use graceful shutdown. - if(!hcs->shutdown_compute_system(vm_name)){ + if (!hcs->shutdown_compute_system(vm_name)) + { // Fall back to SSH shutdown. ssh_exec("sudo shutdown -h now"); } @@ -509,8 +522,9 @@ std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const const VMMount& mount) { mpl::debug(kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name, target); - - throw NotImplementedOnThisBackendException{"Plan9 mounts require an agent running on guest, which is not implemented yet."}; + + throw NotImplementedOnThisBackendException{ + "Plan9 mounts require an agent running on guest, which is not implemented yet."}; // return std::make_unique(this, &key_provider, mount, target, hcs); } diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h index b28b8b2f32..c3e0204fee 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -93,7 +93,14 @@ struct HCSVirtualMachine final : public BaseVirtualMachine hcs::ComputeSystemState fetch_state_from_api(); void set_state(hcs::ComputeSystemState state); - void maybe_create_compute_system(); + + /** + * Create the compute system if it's not already present. + * + * @return true The compute system was absent and created + * @return false The compute system is already present + */ + bool maybe_create_compute_system() noexcept(false); }; } // namespace multipass::hyperv From e1b34c9d00fa9184eee5fa7f645f41b96e5aa7bb Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 27 Mar 2025 01:22:17 +0300 Subject: [PATCH 22/76] [platform_win] Re-implement get_network_interfaces_info natively Re-implement the get_network_interfaces_info using native API in order to avoid relying on Powershell. --- src/platform/platform_win.cpp | 139 +++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 26 deletions(-) diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index d914d4cd01..e419e2b131 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "backends/hyperv/hyperv_virtual_machine_factory.h" #include "backends/hyperv_api/hcs_virtual_machine_factory.h" @@ -36,7 +37,6 @@ #include #include - #include #include #include @@ -51,8 +51,10 @@ #include #include +#include #include + #include #include #include @@ -476,39 +478,124 @@ std::filesystem::path multipass_final_storage_location() } } // namespace +// std::map mp::platform::Platform::get_network_interfaces_info() const +// { +// static const auto ps_cmd_base = QStringLiteral( +// "Get-NetAdapter -physical | Select-Object -Property Name,MediaType,PhysicalMediaType,InterfaceDescription"); +// static const auto ps_args = QString{ps_cmd_base}.split(' ', Qt::SkipEmptyParts) + +// PowerShell::Snippets::to_bare_csv; + +// QString ps_output; +// QString ps_output_err; +// if (PowerShell::exec(ps_args, "Network Listing on Windows Platform", &ps_output, &ps_output_err)) +// { +// std::map ret{}; +// for (const auto& line : ps_output.split(QRegularExpression{"[\r\n]"}, Qt::SkipEmptyParts)) +// { +// auto terms = line.split(',', Qt::KeepEmptyParts); +// if (terms.size() != 4) +// { +// throw std::runtime_error{ +// fmt::format("Could not determine available networks - unexpected powershell output: {}", +// ps_output)}; +// } + +// auto iface = mp::NetworkInterfaceInfo{terms[0].toStdString(), +// interpret_net_type(terms[1], terms[2]), +// terms[3].toStdString()}; +// ret.emplace(iface.id, iface); +// } + +// return ret; +// } + +// auto detail = ps_output_err.isEmpty() ? "" : fmt::format(" Detail: {}", ps_output_err); +// auto err = fmt::format("Could not determine available networks - error executing powershell command.{}", detail); +// throw std::runtime_error{err}; +// } + + +struct GetNetworkInterfacesInfoException : public multipass::FormattedExceptionBase<> +{ + using multipass::FormattedExceptionBase<>::FormattedExceptionBase; +}; + std::map mp::platform::Platform::get_network_interfaces_info() const { - static const auto ps_cmd_base = QStringLiteral( - "Get-NetAdapter -physical | Select-Object -Property Name,MediaType,PhysicalMediaType,InterfaceDescription"); - static const auto ps_args = QString{ps_cmd_base}.split(' ', Qt::SkipEmptyParts) + PowerShell::Snippets::to_bare_csv; - QString ps_output; - QString ps_output_err; - if (PowerShell::exec(ps_args, "Network Listing on Windows Platform", &ps_output, &ps_output_err)) + std::map ret{}; + + auto adapter_type_to_str = [](int type) { + switch (type) + { + case MIB_IF_TYPE_OTHER: + return "Other"; + case MIB_IF_TYPE_ETHERNET: + return "Ethernet"; + case MIB_IF_TYPE_TOKENRING: + return "Token Ring"; + case MIB_IF_TYPE_FDDI: + return "FDDI"; + case MIB_IF_TYPE_PPP: + return "PPP"; + case MIB_IF_TYPE_LOOPBACK: + return "Loopback"; + case MIB_IF_TYPE_SLIP: + return "Slip"; + default: + return "Unknown"; + } + }; + + // TODO: Move to platform? + auto wchar_to_utf8 = [](std::wstring_view input) -> std::string { + if (input.empty()) + return {}; + + const auto size_needed = + WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast(input.size()), nullptr, 0, nullptr, nullptr); + std::string result(size_needed, 0); + WideCharToMultiByte(CP_UTF8, + 0, + input.data(), + static_cast(input.size()), + result.data(), + size_needed, + nullptr, + nullptr); + // FIXME : Check error code and GetLastError here. + return result; + }; + + ULONG needed_size{0}; + // Learn how much space we need to allocate. + GetAdaptersAddresses(AF_UNSPEC, 0, NULL, nullptr, &needed_size); + + auto adapters_info_raw_storage = std::make_unique(needed_size); + + auto adapter_info = reinterpret_cast(adapters_info_raw_storage.get()); + + if (const auto result = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, adapter_info, &needed_size); result == NO_ERROR) { - std::map ret{}; - for (const auto& line : ps_output.split(QRegularExpression{"[\r\n]"}, Qt::SkipEmptyParts)) + // Retrieval was successful. The API returns a linked list, so walk over it. + for (auto pitr = adapter_info; pitr; pitr = pitr->Next) { - auto terms = line.split(',', Qt::KeepEmptyParts); - if (terms.size() != 4) - { - throw std::runtime_error{ - fmt::format("Could not determine available networks - unexpected powershell output: {}", - ps_output)}; - } + const auto& adapter = *pitr; - auto iface = mp::NetworkInterfaceInfo{terms[0].toStdString(), - interpret_net_type(terms[1], terms[2]), - terms[3].toStdString()}; - ret.emplace(iface.id, iface); - } + mp::NetworkInterfaceInfo test; + test.id = wchar_to_utf8(adapter.FriendlyName); + test.type = adapter_type_to_str(adapter.IfType); + test.description = wchar_to_utf8(adapter.Description); - return ret; + ret.insert(std::make_pair(test.id, test)); + } } - - auto detail = ps_output_err.isEmpty() ? "" : fmt::format(" Detail: {}", ps_output_err); - auto err = fmt::format("Could not determine available networks - error executing powershell command.{}", detail); - throw std::runtime_error{err}; + else + { + // TODO: FormatMessage. + throw GetNetworkInterfacesInfoException{"Failed to retrieve network interface information. Error code: {}", result}; + } + return ret; } bool mp::platform::Platform::is_backend_supported(const QString& backend) const From 9ab7a1a3c5faa64d693c7cbe3fb53e53c09dc2ab Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 27 Mar 2025 01:23:26 +0300 Subject: [PATCH 23/76] [hyperv-api-factory] implement networks function --- .../hcs_virtual_machine_factory.cpp | 23 +++++++++++++++++++ .../hyperv_api/hcs_virtual_machine_factory.h | 12 ++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 6fec2c5326..f6b13ecb65 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -24,9 +24,11 @@ #include #include +#include #include #include + #include #include // for the std::filesystem::path formatter @@ -217,4 +219,25 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::clone_vm_impl(const std::string& return create_virtual_machine(desc, key_provider, monitor); } +std::vector HCSVirtualMachineFactory::get_adapters() +{ + std::vector ret; + for (auto& item : MP_PLATFORM.get_network_interfaces_info()) + { + auto& net = item.second; + if (const auto& type = net.type; type == "Ethernet") + { + net.needs_authorization = true; + ret.emplace_back(std::move(net)); + } + } + + return ret; +} + +std::vector HCSVirtualMachineFactory::networks() const +{ + return get_adapters(); +} + } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h index f8493d518d..ff00fac63d 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -49,11 +49,8 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory return "hyperv_api"; }; - std::vector networks() const override - { - return {}; - } - + std::vector networks() const override; + void require_snapshots_support() const override { } @@ -75,6 +72,11 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory hcs_sptr_t hcs_sptr{nullptr}; hcn_sptr_t hcn_sptr{nullptr}; virtdisk_sptr_t virtdisk_sptr{nullptr}; + + /** + * Retrieve a list of available network adapters. + */ + static std::vector get_adapters(); }; } // namespace multipass::hyperv From f516fd0b2150243f4a9a1eaf65fdb2d68d1a9433 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 9 Apr 2025 18:10:15 +0300 Subject: [PATCH 24/76] [rpc] compile with /bigobj MSVC has started to complain, which is fixed by passing the /bigobj flag. C:\multipass-private\build\gen\multipass\rpc\multipass.grpc.pb.cc : fatal error C1128: number of sections exceeded object file format limit: compile with /bigobj --- src/rpc/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index e6095860a4..f6598b3e0f 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -18,6 +18,8 @@ file(MAKE_DIRECTORY ${GRPC_GENERATED_SOURCE_DIR}) if(NOT MSVC) add_compile_options(-Wno-error=pedantic) +else() + add_compile_options(/bigobj) endif() generate_grpc_cpp(GRPC_GENERATED_SOURCES ${GRPC_GENERATED_SOURCE_DIR} ${MULTIPASS_PROTOCOL_SPEC}) From 36a223f532ddbd9cd1e637217ebc01864b865532 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 19:09:42 +0300 Subject: [PATCH 25/76] [platform/win] move wsa init helper & utils to platform_win --- include/multipass/platform_win.h | 104 +++++++ .../hyperv_api/hcs_virtual_machine.cpp | 42 +-- src/platform/platform_win.cpp | 283 +++++++++++++++++- 3 files changed, 388 insertions(+), 41 deletions(-) create mode 100644 include/multipass/platform_win.h diff --git a/include/multipass/platform_win.h b/include/multipass/platform_win.h new file mode 100644 index 0000000000..6bd76c81a8 --- /dev/null +++ b/include/multipass/platform_win.h @@ -0,0 +1,104 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_PLATFORM_WIN_H +#define MULTIPASS_PLATFORM_WIN_H + +#include + +#include + +struct WSAData; + +namespace multipass::platform +{ +struct wsa_init_wrapper +{ + wsa_init_wrapper(); + ~wsa_init_wrapper(); + + /** + * Check whether WSA initialization has succeeded. + * + * @return true WSA is initialized successfully + * @return false WSA initialization failed + */ + operator bool() const noexcept + { + return wsa_init_result == 0; + } + +private: + WSAData* wsa_data{nullptr}; + const int wsa_init_result{-1}; +}; + +// --------------------------------------------------------- + +/** + * Parse given GUID string into a GUID struct. + * + * @param guid_str GUID in string form, either 36 characters + * (without braces) or 38 characters (with braces.) + * + * @return GUID The parsed GUID + */ +[[nodiscard]] auto guid_from_string(const std::string& guid_str) -> GUID; + +/** + * Parse given GUID string into a GUID struct. + * + * @param guid_wstr GUID in string form, either 36 characters + * (without braces) or 38 characters (with braces.) + * + * @return GUID The parsed GUID + */ +[[nodiscard]] auto guid_from_wstring(const std::wstring& guid_wstr) -> GUID; + +// --------------------------------------------------------- + +/** + * @brief Convert a GUID to its string representation + * + * @param [in] guid GUID to convert + * @return std::string GUID in string form + */ +[[nodiscard]] auto guid_to_string(const ::GUID& guid) -> std::string; + +// --------------------------------------------------------- + +/** + * @brief Convert a guid to its wide string representation + * + * @param [in] guid GUID to convert + * @return std::wstring GUID in wstring form + */ +[[nodiscard]] auto guid_to_wstring(const ::GUID& guid) -> std::wstring; + +// --------------------------------------------------------- + +/** + * Convert a multi-byte string to a wide-character string. + * + * @param str Multi-byte string + * @return Wide-character equivalent of the given multi-byte string. + */ +[[nodiscard]] auto string_to_wstring(const std::string& str) -> std::wstring; + +} // namespace multipass::platform + +#endif // MULTIPASS_PLATFORM_WIN_H diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 5981d5e0f8..c5607b4c0d 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -26,10 +26,12 @@ #include #include +#include #include #include #include + #include #include @@ -46,6 +48,7 @@ constexpr auto kLogCategory = "HyperV-Virtual-Machine"; constexpr auto kDefaultSSHPort = 22; namespace mpl = multipass::logging; +namespace mpp = multipass::platform; using lvl = mpl::Level; inline auto mac2uuid(std::string mac_addr) @@ -73,37 +76,16 @@ inline auto replace_colon_with_dash(std::string& addr) */ auto resolve_ip_addresses(const std::string& hostname) { + const static mpp::wsa_init_wrapper wsa_context{}; + + std::vector ipv4{}, ipv6{}; mpl::trace(kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`", hostname); - static auto wsa_context = [] { - struct wsa_init_wrapper - { - wsa_init_wrapper() : wsa_data{}, wsa_init_success(WSAStartup(MAKEWORD(2, 2) == 0, &wsa_data)) - { - mpl::debug(kLogCategory, "resolve_ip_addresses() -> initialized WSA, status `{}`", wsa_init_success); - if (!wsa_init_success) - { - mpl::error(kLogCategory, "resolve_ip_addresses() > WSAStartup failed!"); - } - } - ~wsa_init_wrapper() - { - /** - * https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsacleanup - * There must be a call to WSACleanup for each successful call to WSAStartup. - * Only the final WSACleanup function call performs the actual cleanup. - * The preceding calls simply decrement an internal reference count in the WS2_32.DLL. - */ - if (wsa_init_success) - { - WSACleanup(); - } - } - WSADATA wsa_data{}; - const bool wsa_init_success{false}; - }; - return wsa_init_wrapper{}; - }(); + if (!wsa_context) + { + mpl::error(kLogCategory, "resolve_ip_addresses() -> WSA not initialized! `{}`", hostname); + return std::make_pair(ipv4, ipv6); + } // Wrap the raw addrinfo pointer so it's always destroyed properly. const auto& [result, addr_info] = [&]() { @@ -113,8 +95,6 @@ auto resolve_ip_addresses(const std::string& hostname) return std::make_pair(r, std::unique_ptr{result, freeaddrinfo}); }(); - std::vector ipv4{}, ipv6{}; - if (result == 0) { assert(addr_info.get()); diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index e419e2b131..9ce3272bb0 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -49,14 +50,20 @@ #include +#include +#include #include -#include +#include +#include #include +#include +#include +#include #include - #include #include +#include #include #include #include @@ -478,6 +485,114 @@ std::filesystem::path multipass_final_storage_location() } } // namespace +/** + * Formatter for GUID type + */ +template +struct fmt::formatter<::GUID, Char> +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const ::GUID& guid, FormatContext& ctx) const + { + // The format string is laid out char by char to allow it + // to be used for initializing variables with different character + // sizes. + static constexpr Char guid_f[] = {'{', ':', '0', '8', 'x', '}', '-', '{', ':', '0', '4', 'x', '}', '-', '{', + ':', '0', '4', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', + '2', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', + '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', + '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', 0}; + return format_to(ctx.out(), + guid_f, + guid.Data1, + guid.Data2, + guid.Data3, + guid.Data4[0], + guid.Data4[1], + guid.Data4[2], + guid.Data4[3], + guid.Data4[4], + guid.Data4[5], + guid.Data4[6], + guid.Data4[7]); + } +}; + +struct GuidParseError : multipass::FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +auto mp::platform::guid_from_wstring(const std::wstring& guid_wstr) -> ::GUID +{ + constexpr auto kGUIDLength = 36; + constexpr auto kGUIDLengthWithBraces = kGUIDLength + 2; + + const auto input = [&guid_wstr]() { + switch (guid_wstr.length()) + { + case kGUIDLength: + // CLSIDFromString requires GUIDs to be wrapped with braces. + return fmt::format(L"{{{}}}", guid_wstr); + case kGUIDLengthWithBraces: + { + if (*guid_wstr.begin() != L'{' || *std::prev(guid_wstr.end()) != L'}') + { + throw GuidParseError{"GUID string either does not start or end with a brace."}; + } + return guid_wstr; + } + } + throw GuidParseError{"Invalid length for a GUID string ({}).", guid_wstr.length()}; + }(); + + ::GUID guid = {}; + + const auto result = CLSIDFromString(input.c_str(), &guid); + + if (FAILED(result)) + { + throw GuidParseError{"Failed to parse the GUID string ({}).", result}; + } + + return guid; +} + +// --------------------------------------------------------- + +auto mp::platform::string_to_wstring(const std::string& str) -> std::wstring +{ + return std::wstring_convert>().from_bytes(str); +} + +// --------------------------------------------------------- + +auto mp::platform::guid_from_string(const std::string& guid_str) -> GUID +{ + // Just use the wide string overload. + return guid_from_wstring(string_to_wstring(guid_str)); +} + +// --------------------------------------------------------- + +auto mp::platform::guid_to_string(const ::GUID& guid) -> std::string +{ + + return fmt::format("{}", guid); +} + +// --------------------------------------------------------- + +auto mp::platform::guid_to_wstring(const ::GUID& guid) -> std::wstring +{ + return fmt::format(L"{}", guid); +} + // std::map mp::platform::Platform::get_network_interfaces_info() const // { // static const auto ps_cmd_base = QStringLiteral( @@ -514,15 +629,106 @@ std::filesystem::path multipass_final_storage_location() // throw std::runtime_error{err}; // } +mp::platform::wsa_init_wrapper::wsa_init_wrapper() + : wsa_data(new ::WSAData()), wsa_init_result(::WSAStartup(MAKEWORD(2, 2), wsa_data)) +{ + constexpr auto category = "wsa-init-wrapper"; + mpl::debug(category, " initialized WSA, status `{}`", wsa_init_result); + + if(!operator bool()){ + mpl::error(category, " WSAStartup failed with `{}`: {}", std::system_category().message(wsa_init_result)); + } +} + +mp::platform::wsa_init_wrapper::~wsa_init_wrapper() +{ + /** + * https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsacleanup + * There must be a call to WSACleanup for each successful call to WSAStartup. + * Only the final WSACleanup function call performs the actual cleanup. + * The preceding calls simply decrement an internal reference count in the WS2_32.DLL. + */ + if (operator bool()) + { + WSACleanup(); + } + delete wsa_data; +} struct GetNetworkInterfacesInfoException : public multipass::FormattedExceptionBase<> { using multipass::FormattedExceptionBase<>::FormattedExceptionBase; }; -std::map mp::platform::Platform::get_network_interfaces_info() const +struct InvalidNetworkPrefixLengthException : public multipass::FormattedExceptionBase<> +{ + using multipass::FormattedExceptionBase<>::FormattedExceptionBase; +}; + +static const auto& ip_utils() { + static multipass::platform::wsa_init_wrapper wrapper; + struct ip_utils + { + static std::string to_string(std::uint32_t addr) + { + char str[INET_ADDRSTRLEN] = {}; + if (!inet_ntop(AF_INET, &addr, str, sizeof(str))) + throw std::runtime_error("inet_ntop failed: errno"); + return str; + } + static std::string to_string(const in6_addr& addr) + { + char str[INET6_ADDRSTRLEN] = {}; + if (!inet_ntop(AF_INET6, &addr, str, sizeof(str))) + throw std::runtime_error("inet_ntop failed: errno"); + return str; + } + + static auto to_network(const in_addr& v4, std::uint8_t prefix_length) + { + // Convert to the host long first so we can apply a mask to it + constexpr static auto kMaxPrefixLength = 32; + const auto ip_hbo = ntohl(v4.S_un.S_addr); + if (prefix_length > kMaxPrefixLength) + { + throw std::runtime_error{"Given prefix length `{}` is larger than `{}`!"}; + } + const auto mask = + (prefix_length == 0) ? 0 : std::numeric_limits::max() << (32 - prefix_length); + const auto network_hbo = htonl(ip_hbo & mask); + + return fmt::format("{}/{}", to_string(network_hbo), prefix_length); + } + + static auto to_network(const in6_addr& v6, std::uint8_t prefix_length) + { + // Convert to the host long first so we can apply a mask to it + constexpr static auto kMaxPrefixLength = 128; + if (prefix_length > kMaxPrefixLength) + { + throw std::runtime_error{"Given prefix length `{}` is larger than `{}`!"}; + } + in6_addr masked = v6; + + for (int i = 0; i < 16; ++i) + { + int bits = i * 8; + if (prefix_length < bits) + masked.u.Byte[i] = 0; + else if (prefix_length < bits + 8) + masked.u.Byte[i] &= static_cast(0xFF << (8 - (prefix_length - bits))); + } + const auto network_addr = to_string(masked); + return fmt::format("{}/{}", network_addr, prefix_length); + } + } static helper; + return helper; +} + +std::map mp::platform::Platform::get_network_interfaces_info() const +{ std::map ret{}; auto adapter_type_to_str = [](int type) { @@ -567,27 +773,84 @@ std::map mp::platform::Platform::get_netw return result; }; + auto unicast_addr_to_network = [](PIP_ADAPTER_UNICAST_ADDRESS_LH first_unicast_addr) { + std::vector result; + for (const auto* unicast_addr = first_unicast_addr; unicast_addr; unicast_addr = unicast_addr->Next) + { + const auto& sa = *unicast_addr->Address.lpSockaddr; + std::optional network_addr{}; + switch (sa.sa_family) + { + case AF_INET: + network_addr = ip_utils().to_network(reinterpret_cast(&sa)->sin_addr, + unicast_addr->OnLinkPrefixLength); + break; + case AF_INET6: + network_addr = ip_utils().to_network(reinterpret_cast(&sa)->sin6_addr, + unicast_addr->OnLinkPrefixLength); + break; + } + + if (network_addr) + { + result.emplace_back(std::move(network_addr.value())); + } + } + return result; + }; + ULONG needed_size{0}; + constexpr auto flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_ALL_INTERFACES; // Learn how much space we need to allocate. - GetAdaptersAddresses(AF_UNSPEC, 0, NULL, nullptr, &needed_size); + GetAdaptersAddresses(AF_UNSPEC, flags, NULL, nullptr, &needed_size); auto adapters_info_raw_storage = std::make_unique(needed_size); auto adapter_info = reinterpret_cast(adapters_info_raw_storage.get()); - if (const auto result = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, adapter_info, &needed_size); result == NO_ERROR) + if (const auto result = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapter_info, &needed_size); result == NO_ERROR) { // Retrieval was successful. The API returns a linked list, so walk over it. for (auto pitr = adapter_info; pitr; pitr = pitr->Next) { const auto& adapter = *pitr; - mp::NetworkInterfaceInfo test; - test.id = wchar_to_utf8(adapter.FriendlyName); - test.type = adapter_type_to_str(adapter.IfType); - test.description = wchar_to_utf8(adapter.Description); + MIB_IF_ROW2 ifRow{}; + ifRow.InterfaceLuid = adapter.Luid; + if (GetIfEntry2(&ifRow) != NO_ERROR) { + continue; + } + + // Only list the physical interfaces. + if(!ifRow.InterfaceAndOperStatusFlags.HardwareInterface){ + continue; + } + + mp::NetworkInterfaceInfo net{}; + net.id = wchar_to_utf8(adapter.FriendlyName); + net.type = adapter_type_to_str(adapter.IfType); + net.description = wchar_to_utf8(adapter.Description); + net.links = unicast_addr_to_network(adapter.FirstUnicastAddress); + ret.insert(std::make_pair(net.id, net)); + } - ret.insert(std::make_pair(test.id, test)); + // Host compute system API requires the original subnet. + for (auto& [name, netinfo] : ret) + { + if (netinfo.links.empty()) + { + const std::wstring search = fmt::format(L"vEthernet ({})", string_to_wstring(netinfo.id)); + for (auto pitr = adapter_info; pitr; pitr = pitr->Next) + { + const auto& adapter = *pitr; + std::wstring name{adapter.FriendlyName}; + + if(name == search){ + netinfo.links = unicast_addr_to_network(adapter.FirstUnicastAddress); + break; + } + } + } } } else From 255d297c56a84f9a281c099e590d01572728cbae Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 19:12:45 +0300 Subject: [PATCH 26/76] [hyperv-api-factory] implement create_bridge_with function --- .../hcs_virtual_machine_factory.cpp | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index f6b13ecb65..58a7eced55 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -18,9 +18,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -28,11 +30,12 @@ #include #include - #include #include // for the std::filesystem::path formatter +#include + namespace multipass::hyperv { @@ -41,6 +44,12 @@ namespace multipass::hyperv */ static constexpr auto kLogCategory = "HyperV-Virtual-Machine-Factory"; static constexpr auto kDefaultHyperVSwitchGUID = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; +static constexpr auto kExtraInterfaceBridgeNameFmtStr = "Multipass Bridge ({})"; +/** + * Regex pattern to extract the origin network name and GUID from an extra interface + * name. + */ +static constexpr auto kExtraInterfaceBridgeNameRegex = "Multipass Bridge \\((.*)\\)"; // Delegating constructor HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) @@ -166,9 +175,24 @@ void HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_im std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) { - (void)intf; + const auto bridge_name = fmt::format(kExtraInterfaceBridgeNameFmtStr, intf.id); + const auto params = [&intf, &bridge_name] { + hcn::CreateNetworkParameters network_params{}; + network_params.name = bridge_name; + network_params.type = hcn::HcnNetworkType::Transparent(); + network_params.guid = multipass::utils::make_uuid(network_params.name).toStdString(); + network_params.policies.NetAdapterName = intf.id; + return network_params; + }(); + + assert(hcn_sptr); + const auto& [status, status_msg] = hcn_sptr->create_network(params); + + if (status || static_cast(status) == HCN_E_NETWORK_ALREADY_EXISTS) + { + return params.name; + } - // No-op. The implementation uses the default Hyper-V switch. return {}; } @@ -207,7 +231,7 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::clone_vm_impl(const std::string& virtdisk::CreateVirtualDiskParameters clone_vhdx_params{}; clone_vhdx_params.source = src_vm_vhdx.value(); clone_vhdx_params.path = desc.image.image_path.toStdString(); - clone_vhdx_params.size_in_bytes = 0; // use source + clone_vhdx_params.size_in_bytes = 0; // use source disk size const auto& [status, msg] = virtdisk_sptr->create_virtual_disk(clone_vhdx_params); From f9a37f33bb3ac9df7212de5f2c7f19d75a143499 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 19:13:20 +0300 Subject: [PATCH 27/76] [hyperv-api-factory] ensure bridge(s) are created on vm creation --- .../hcs_virtual_machine_factory.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 58a7eced55..cb90a0ec9d 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -86,6 +86,39 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine(const Virt assert(hcn_sptr); assert(virtdisk_sptr); + const auto networks = MP_PLATFORM.get_network_interfaces_info(); + for (const auto& extra : desc.extra_interfaces) + { + std::regex pattern{kExtraInterfaceBridgeNameRegex}; + std::smatch match; + + // The origin interface name is encoded into the interface name itself. + if (!std::regex_match(extra.id, match, pattern) || match.size() != 2) + { + mpl::error(kLogCategory, "Invalid extra interface name `{}`.", extra.id); + continue; + } + + const auto origin_interface_name = match[1].str(); + const auto origin_network_guid = match[2].str(); + + const auto found = std::find_if(networks.begin(), networks.end(), [&](const auto& kvp) { + const auto& [k, v] = kvp; + return v.id == origin_interface_name; + }); + + if (networks.end() == found) + { + mpl::warn(kLogCategory, + "Could not find the source interface `{}` for extra `{}`", + origin_interface_name, + extra.id); + continue; + } + + create_bridge_with(found->second); + } + return std::make_unique(hcs_sptr, hcn_sptr, virtdisk_sptr, From 9e2b70df8e97007c5be59a71946676745508355f Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 21:04:21 +0300 Subject: [PATCH 28/76] [hyperv-hcn] overhaul create_network implementation the create_network implementation was only good for spawning ICS networks which is not sufficient for other needs, such as creating a transparent network. the following changes has been made in order to properly handle creation of different kind of networks: - Added the following types, representing HCN network object structure: - HcnNetworkType - HcnIpam - HcnIpamType - HcnNetworkPolicy - HcnSubnet - HcnNetworkFlags - HcnNetworkPolicyNetAdapterName - HcnNetworkPolicyType - HcnRoute - The code now uses "ipams" style network declarations - Added proper formatter/renderers for the new types - Updated the test code - Switched to named format arguments --- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 180 ++++++++++++++++-- .../hcn/hyperv_hcn_create_network_params.h | 36 +++- .../backends/hyperv_api/hcn/hyperv_hcn_ipam.h | 65 +++++++ .../hyperv_api/hcn/hyperv_hcn_ipam_type.h | 59 ++++++ .../hyperv_api/hcn/hyperv_hcn_network_flags.h | 105 ++++++++++ .../hcn/hyperv_hcn_network_policy.h | 71 +++++++ ...hyperv_hcn_network_policy_netadaptername.h | 53 ++++++ .../hcn/hyperv_hcn_network_policy_type.h | 167 ++++++++++++++++ .../hyperv_api/hcn/hyperv_hcn_network_type.h | 135 +++++++++++++ .../hyperv_api/hcn/hyperv_hcn_route.h | 69 +++++++ .../hyperv_api/hcn/hyperv_hcn_subnet.h | 61 ++++++ .../hcs_virtual_machine_factory.cpp | 5 +- tests/hyperv_api/test_bb_cit_hyperv.cpp | 8 +- tests/hyperv_api/test_it_hyperv_hcn_api.cpp | 9 +- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 34 ++-- 15 files changed, 1005 insertions(+), 52 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_type.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index e23ff2a382..87deaf7c06 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -133,6 +133,141 @@ UniqueHcnNetwork open_network(const HCNAPITable& api, const std::string& network return UniqueHcnNetwork{network, api.CloseNetwork}; } +/** + * Determine the log severity level for a HCN error. + * + * @param [in] result Operation result + * @return mpl::Level The determined severity level + */ +auto hcn_errc_to_log_level(const OperationResult& result) +{ + /** + * Some of the errors are "expected", e.g. a network may be already + * exist and that's not necessarily an error. + */ + switch (static_cast(result.code)) + { + case HCN_E_NETWORK_ALREADY_EXISTS: + return mpl::Level::debug; + } + + return mpl::Level::error; +} + +/** + * For each element in @p elem, apply @p op and + * aggregate the result. Then, join all by comma. + * + * @tparam T Element type + * @tparam Func Function type + * @param [in] elems Elements + * @param [in] op Operation to apply + * @return Comma-separated list of transformed elements + */ +template +auto transform_join(const std::vector& elems, Func op) +{ + std::vector result; + std::transform(elems.begin(), elems.end(), std::back_inserter(result), op); + return fmt::format(L"{}", fmt::join(result, L",")); +} + +/** + * Format a HcnRoute + * + * @param [in] route Route to format + * @return HCN JSON representation of @p route + */ +std::wstring format_route(const HcnRoute& route) +{ + constexpr auto route_template = LR"""( + {{ + "NextHop": "{NextHop}", + "DestinationPrefix": "{DestinationPrefix}", + "Metric": {Metric} + }} + )"""; + + return fmt::format(route_template, + fmt::arg(L"NextHop", string_to_wstring(route.next_hop)), + fmt::arg(L"DestinationPrefix", string_to_wstring(route.destination_prefix)), + fmt::arg(L"Metric", route.metric)); +} + +/** + * Format a HcnSubnet + * + * @param [in] subnet Subnet to format + * @return HCN JSON representation of @p subnet + */ +std::wstring format_subnet(const HcnSubnet& subnet) +{ + constexpr auto subnet_template = LR"""( + {{ + "Policies": [], + "Routes" : [ + {Routes} + ], + "IpAddressPrefix" : "{IpAddressPrefix}", + "IpSubnets": null + }} + )"""; + + return fmt::format(subnet_template, + fmt::arg(L"IpAddressPrefix", string_to_wstring(subnet.ip_address_prefix)), + fmt::arg(L"Routes", transform_join(subnet.routes, format_route))); +} + +/** + * Format a HcnIpam + * + * @param [in] ipam IPAM to format + * @return HCN JSON representation of @p ipam + */ +std::wstring format_ipam(const HcnIpam& ipam) +{ + constexpr auto ipam_template = LR"""( + {{ + "Type": "{Type}", + "Subnets": [ + {Subnets} + ] + }} + )"""; + + return fmt::format(ipam_template, + fmt::arg(L"Type", string_to_wstring(std::string{ipam.type})), + fmt::arg(L"Subnets", transform_join(ipam.subnets, format_subnet))); +} + +struct NetworkPolicySettingsFormatters +{ + std::wstring operator()(const HcnNetworkPolicyNetAdapterName& policy) + { + constexpr auto netadaptername_settings_template = LR"""( + "NetworkAdapterName": "{NetworkAdapterName}" + )"""; + + return fmt::format(netadaptername_settings_template, + fmt::arg(L"NetworkAdapterName", string_to_wstring(policy.net_adapter_name))); + } +}; + +std::wstring format_network_policy(const HcnNetworkPolicy& policy) +{ + constexpr auto network_policy_template = LR"""( + {{ + "Type": "{Type}", + "Settings": {{ + {Settings} + }} + }} + )"""; + return fmt::format(network_policy_template, + fmt::arg(L"Type", string_to_wstring(policy.type)), + fmt::arg(L"Settings", std::visit(NetworkPolicySettingsFormatters{}, policy.settings))); +} + } // namespace // --------------------------------------------------------- @@ -153,29 +288,31 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params */ constexpr auto network_settings_template = LR"""( {{ - "Name": "{0}", - "Type": "ICS", - "Subnets" : [ - {{ - "GatewayAddress": "{2}", - "AddressPrefix" : "{1}", - "IpSubnets" : [ - {{ - "IpAddressPrefix": "{1}" - }} - ] - }} + "SchemaVersion": + {{ + "Major": 2, + "Minor": 2 + }}, + "Name": "{Name}", + "Type": "{Type}", + "Ipams": [ + {Ipams} ], - "IsolateSwitch": true, - "Flags" : 265 + "Flags": {Flags}, + "Policies": [ + {Policies} + ] }} )"""; // Render the template - const auto network_settings = fmt::format(network_settings_template, - string_to_wstring(params.name), - string_to_wstring(params.subnet), - string_to_wstring(params.gateway)); + const auto network_settings = + fmt::format(network_settings_template, + fmt::arg(L"Name", string_to_wstring(params.name)), + fmt::arg(L"Type", string_to_wstring(std::string{params.type})), + fmt::arg(L"Flags", fmt::underlying(params.flags)), + fmt::arg(L"Ipams", transform_join(params.ipams, format_ipam)), + fmt::arg(L"Policies", transform_join(params.policies, format_network_policy))); HCN_NETWORK network{nullptr}; const auto result = perform_hcn_operation(api, @@ -186,8 +323,11 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params if (!result) { - // FIXME: Also include the result error message, if any. - mpl::error(kLogCategory, "HCNWrapper::create_network(...) > HcnCreateNetwork failed with {}!", result.code); + mpl::log(hcn_errc_to_log_level(result), + kLogCategory, + "HCNWrapper::create_network(...) > HcnCreateNetwork failed with {}: {}", + result.code, + std::system_category().message(static_cast(result.code))); } [[maybe_unused]] UniqueHcnNetwork _{network, api.CloseNetwork}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.h index feb0a32a86..8e03aea9dd 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.h @@ -18,8 +18,14 @@ #ifndef MULTIPASS_HYPERV_API_HCN_CREATE_NETWORK_PARAMETERS_H #define MULTIPASS_HYPERV_API_HCN_CREATE_NETWORK_PARAMETERS_H +#include +#include +#include +#include + #include -#include + +#include namespace multipass::hyperv::hcn { @@ -34,21 +40,30 @@ struct CreateNetworkParameters */ std::string name{}; + /** + * Type of the network + */ + HcnNetworkType type{HcnNetworkType::Ics()}; + + /** + * Flags for the network. + */ + HcnNetworkFlags flags{HcnNetworkFlags::none}; + /** * RFC4122 unique identifier for the network. */ std::string guid{}; /** - * Subnet CIDR that defines the address space of - * the network. + * IP Address Management */ - std::string subnet{}; + std::vector ipams{}; /** - * The default gateway address for the network. + * Network policies */ - std::string gateway{}; + std::vector policies; }; } // namespace multipass::hyperv::hcn @@ -68,11 +83,14 @@ struct fmt::formatter auto format(const multipass::hyperv::hcn::CreateNetworkParameters& params, FormatContext& ctx) const { return format_to(ctx.out(), - "Network Name: ({}) | Network GUID: ({}) | Subnet CIDR: ({}) | Gateway Addr.: ({}) ", + "Network Name: ({}) | Network Type: ({}) | Network GUID: ({}) | Flags: ({}) | IPAMs: ({}) | " + "Policies: ({})", params.name, + static_cast(params.type), params.guid, - params.subnet, - params.gateway); + params.flags, + fmt::join(params.ipams, ","), + fmt::join(params.policies, ",")); } }; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h new file mode 100644 index 0000000000..e30e538297 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h @@ -0,0 +1,65 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_IPAM_H +#define MULTIPASS_HYPERV_API_HCN_IPAM_H + +#include +#include + +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnIpam +{ + /** + * Type of the IPAM + */ + HcnIpamType type{HcnIpamType::Static()}; + + /** + * Defined subnet ranges for the IPAM + */ + std::vector subnets; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnIpam + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcn::HcnIpam& ipam, FormatContext& ctx) const + { + return format_to(ctx.out(), + "Type: ({}) | Subnets: ({})", + static_cast(ipam.type), + fmt::join(ipam.subnets, ",")); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h new file mode 100644 index 0000000000..0dd974d579 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h @@ -0,0 +1,59 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_IPAM_TYPE_H +#define MULTIPASS_HYPERV_API_HCN_IPAM_TYPE_H + +#include +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnIpamType +{ + operator std::string_view() const + { + return value; + } + + operator std::string() const + { + return std::string{value}; + } + + static inline const auto Dhcp() + { + return HcnIpamType{"DHCP"}; + } + + static inline const auto Static() + { + return HcnIpamType{"static"}; + } + +private: + HcnIpamType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h new file mode 100644 index 0000000000..5ed952b2cf --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h @@ -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 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_NETWORK_FLAGS_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_FLAGS_H + +#include +#include + +#include +#include +#include + +namespace multipass::hyperv::hcn +{ + +/** + * https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/51b2c0024ce9fc0c9c240fe8e14b170e05c57099/virtualization/api/hcn/HNS_Schema.md?plain=1#L486 + */ +enum class HcnNetworkFlags : std::uint32_t +{ + none = 0, ///< 2.0 + enable_dns_proxy = 1 << 0, ///< 2.0 + enable_dhcp_server = 1 << 1, ///< 2.0 + enable_mirroring = 1 << 2, ///< 2.0 + enable_non_persistent = 1 << 3, ///< 2.0 + isolate_vswitch = 1 << 4, ///< 2.0 + enable_flow_steering = 1 << 5, ///< 2.11 + disable_sharing = 1 << 6, ///< 2.14 + enable_firewall = 1 << 7, ///< 2.14 + disable_host_port = 1 << 10, ///< ?? + enable_iov = 1 << 13, ///< ?? +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnNetworkFlags + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(multipass::hyperv::hcn::HcnNetworkFlags flags, FormatContext& ctx) const + { + std::vector parts; + + auto is_flag_set = [](decltype(flags) flags, decltype(flags) flag) { + const auto flags_u = fmt::underlying(flags); + const auto flag_u = fmt::underlying(flag); + return flags_u & flag_u; + }; + + if (flags == decltype(flags)::none) + { + parts.emplace_back("none"); + } + else + { + if (is_flag_set(flags, decltype(flags)::enable_dns_proxy)) + parts.emplace_back("enable_dns_proxy"); + if (is_flag_set(flags, decltype(flags)::enable_dhcp_server)) + parts.emplace_back("enable_dhcp_server"); + if (is_flag_set(flags, decltype(flags)::enable_mirroring)) + parts.emplace_back("enable_mirroring"); + if (is_flag_set(flags, decltype(flags)::enable_non_persistent)) + parts.emplace_back("enable_non_persistent"); + if (is_flag_set(flags, decltype(flags)::isolate_vswitch)) + parts.emplace_back("isolate_vswitch"); + if (is_flag_set(flags, decltype(flags)::enable_flow_steering)) + parts.emplace_back("enable_flow_steering"); + if (is_flag_set(flags, decltype(flags)::disable_sharing)) + parts.emplace_back("disable_sharing"); + if (is_flag_set(flags, decltype(flags)::enable_firewall)) + parts.emplace_back("enable_firewall"); + if (is_flag_set(flags, decltype(flags)::disable_host_port)) + parts.emplace_back("disable_host_port"); + if (is_flag_set(flags, decltype(flags)::enable_iov)) + parts.emplace_back("enable_iov"); + } + + return format_to(ctx.out(), "{}", fmt::join(parts, " | ")); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h new file mode 100644 index 0000000000..38fba8cd28 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h @@ -0,0 +1,71 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_H + +#include +#include + +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnNetworkPolicy +{ + /** + * The type of the network policy. + */ + HcnNetworkPolicyType type; + + /** + * Right now, there's only one policy type defined but + * it might expand in the future, so let's go an extra + * mile to future-proof this code. + */ + std::variant settings; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnNetworkPolicy + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcn::HcnNetworkPolicy& params, FormatContext& ctx) const + { + return std::visit( + [&](const auto& settings) { + return fmt::format_to(ctx.out(), + "Type: {} | Settings: {}", + static_cast(params.type), + settings); + }, + params.settings); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h new file mode 100644 index 0000000000..50548e542c --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h @@ -0,0 +1,53 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_NETADAPTERNAME_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_NETADAPTERNAME_H + +#include + +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnNetworkPolicyNetAdapterName +{ + std::string net_adapter_name; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnNetworkPolicyNetAdapterName + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName& policy, FormatContext& ctx) const + { + return format_to(ctx.out(), "NetAdapterName: ({})", policy.net_adapter_name); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h new file mode 100644 index 0000000000..e158da3107 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h @@ -0,0 +1,167 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_TYPE_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_TYPE_H + +#include +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Strongly-typed string values for + * network policy types. + * + * @ref https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/51b2c0024ce9fc0c9c240fe8e14b170e05c57099/virtualization/api/hcn/HNS_Schema.md?plain=1#L522 + */ +struct HcnNetworkPolicyType +{ + operator std::string_view() const + { + return value; + } + + operator std::string() const + { + return std::string{value}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto SourceMacAddress() + { + return HcnNetworkPolicyType{"SourceMacAddress"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto NetAdapterName() + { + return HcnNetworkPolicyType{"NetAdapterName"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto VSwitchExtension() + { + return HcnNetworkPolicyType{"VSwitchExtension"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto DrMacAddress() + { + return HcnNetworkPolicyType{"DrMacAddress"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto AutomaticDNS() + { + return HcnNetworkPolicyType{"AutomaticDNS"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto InterfaceConstraint() + { + return HcnNetworkPolicyType{"InterfaceConstraint"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto ProviderAddress() + { + return HcnNetworkPolicyType{"ProviderAddress"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto RemoteSubnetRoute() + { + return HcnNetworkPolicyType{"RemoteSubnetRoute"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto VxlanPort() + { + return HcnNetworkPolicyType{"VxlanPort"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto HostRoute() + { + return HcnNetworkPolicyType{"HostRoute"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto SetPolicy() + { + return HcnNetworkPolicyType{"SetPolicy"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto NetworkL4Proxy() + { + return HcnNetworkPolicyType{"NetworkL4Proxy"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto LayerConstraint() + { + return HcnNetworkPolicyType{"LayerConstraint"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto NetworkACL() + { + return HcnNetworkPolicyType{"NetworkACL"}; + } + +private: + constexpr HcnNetworkPolicyType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_type.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_type.h new file mode 100644 index 0000000000..baaa4ced48 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_type.h @@ -0,0 +1,135 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_NETWORK_TYPE_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_TYPE_H + +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Strongly-typed string values for + * network type. + */ +struct HcnNetworkType +{ + operator std::string_view() const + { + return value; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Nat() + { + return HcnNetworkType{"NAT"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Ics() + { + return HcnNetworkType{"ICS"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Transparent() + { + return HcnNetworkType{"Transparent"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto L2Bridge() + { + return HcnNetworkType{"L2Bridge"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto L2Tunnel() + { + return HcnNetworkType{"L2Tunnel"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Overlay() + { + return HcnNetworkType{"Overlay"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Private() + { + return HcnNetworkType{"Private"}; + } + + /** + * @since Version 2.0 + */ + constexpr static auto Internal() + { + return HcnNetworkType{"Internal"}; + } + + /** + * @since Version 2.4 + */ + constexpr static auto Mirrored() + { + return HcnNetworkType{"Mirrored"}; + } + + /** + * @since Version 2.4 + */ + constexpr static auto Infiniband() + { + return HcnNetworkType{"Infiniband"}; + } + + /** + * @since Version 2.10 + */ + constexpr static auto ConstrainedICS() + { + return HcnNetworkType{"ConstrainedICS"}; + } + +private: + constexpr HcnNetworkType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h new file mode 100644 index 0000000000..6e6978c2f4 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h @@ -0,0 +1,69 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_ROUTE_H +#define MULTIPASS_HYPERV_API_HCN_ROUTE_H + +#include + +#include +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnRoute +{ + /** + * IP Address of the next hop gateway + */ + std::string next_hop{}; + /** + * IP Prefix in CIDR + */ + std::string destination_prefix{}; + /** + * Route metric + */ + std::uint8_t metric{0}; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnRoute + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcn::HcnRoute& route, FormatContext& ctx) const + { + return format_to(ctx.out(), + "Next Hop: ({}) | Destination Prefix: ({}) | Metric: ({})", + route.next_hop, + route.destination_prefix, + route.metric); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h new file mode 100644 index 0000000000..28e9458deb --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h @@ -0,0 +1,61 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_SUBNET_H +#define MULTIPASS_HYPERV_API_HCN_SUBNET_H + +#include + +#include +#include + +#include +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnSubnet +{ + std::string ip_address_prefix{}; + std::vector routes{}; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnSubnet + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::hcn::HcnSubnet& subnet, FormatContext& ctx) const + { + return format_to(ctx.out(), + "IP Address Prefix: ({}) | Routes: ({})", + subnet.ip_address_prefix, + fmt::join(subnet.routes, ",")); + } +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index cb90a0ec9d..0b96bbf51e 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -214,7 +214,10 @@ std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceI network_params.name = bridge_name; network_params.type = hcn::HcnNetworkType::Transparent(); network_params.guid = multipass::utils::make_uuid(network_params.name).toStdString(); - network_params.policies.NetAdapterName = intf.id; + + hcn::HcnNetworkPolicy policy{hcn::HcnNetworkPolicyType::NetAdapterName(), + hcn::HcnNetworkPolicyNetAdapterName{intf.id}}; + network_params.policies.push_back(policy); return network_params; }(); diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index eb49f39e79..9a6110387a 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -49,8 +49,8 @@ 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.subnet = "10.99.99.0/24"; - network_parameters.gateway = "10.99.99.1"; + network_parameters.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; return network_parameters; }(); @@ -134,8 +134,8 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo hyperv::hcn::CreateNetworkParameters network_parameters{}; network_parameters.name = "multipass-hyperv-cit"; network_parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47486"; - network_parameters.subnet = "10.99.99.0/24"; - network_parameters.gateway = "10.99.99.1"; + network_parameters.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; return network_parameters; }(); diff --git a/tests/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/hyperv_api/test_it_hyperv_hcn_api.cpp index 333300186c..7b1119f630 100644 --- a/tests/hyperv_api/test_it_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -17,6 +17,7 @@ #include "tests/common.h" +#include #include #include #include @@ -36,8 +37,8 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_network) hyperv::hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-delete-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + params.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; (void)uut.delete_network(params.guid); @@ -60,8 +61,8 @@ TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) hyperv::hcn::CreateNetworkParameters network_params{}; network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; - network_params.subnet = "172.50.224.0/20"; - network_params.gateway = "172.50.224.1"; + network_params.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; hyperv::hcn::CreateEndpointParameters endpoint_params{}; diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index 5312a45846..bc18ffa052 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -132,21 +132,27 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success) [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { constexpr auto expected_network_settings = LR"""( { + "SchemaVersion": { + "Major": 2, + "Minor": 2 + }, "Name": "multipass-hyperv-api-hcn-create-test", "Type": "ICS", - "Subnets" : [ + "Ipams": [ { - "GatewayAddress": "172.50.224.1", - "AddressPrefix" : "172.50.224.0/20", - "IpSubnets" : [ + "Type": "static", + "Subnets": [ { - "IpAddressPrefix": "172.50.224.0/20" + "Policies": [], + "Routes": [], + "IpAddressPrefix": "172.50.224.0/20", + "IpSubnets": null } ] } ], - "IsolateSwitch": true, - "Flags" : 265 + "Flags" : 0, + "Policies": [] } )"""; ASSERT_NE(nullptr, network); @@ -174,8 +180,8 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success) hyperv::hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + params.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; const auto& [status, status_msg] = uut.create_network(params); ASSERT_TRUE(status); @@ -226,8 +232,8 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_close_network_failed) hyperv::hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + params.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; uut_t uut{mock_api_table}; const auto& [success, error_msg] = uut.create_network(params); @@ -277,7 +283,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_failed) logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_operation(...)"); logger_scope.mock_logger->expect_log( mpl::Level::error, - "HCNWrapper::create_network(...) > HcnCreateNetwork failed with 0x80004003!"); + "HCNWrapper::create_network(...) > HcnCreateNetwork failed with 0x80004003"); } /****************************************************** @@ -287,8 +293,8 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_failed) hyperv::hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + params.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; uut_t uut{mock_api_table}; const auto& [success, error_msg] = uut.create_network(params); From dec1c441e383540021f2c821a82c943e9ccab1ae Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 21:31:02 +0300 Subject: [PATCH 29/76] [hyperv-hcn] add "MacAddress" to the endpoint creation parameters --- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 11 ++++++++--- .../hcn/hyperv_hcn_create_endpoint_params.h | 15 +++++++++++++-- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 6 ++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 87deaf7c06..6fc5b54a0d 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -364,12 +364,17 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para "Major": 2, "Minor": 16 }}, - "HostComputeNetwork": "{0}", - "Policies": [] + "HostComputeNetwork": "{HostComputeNetwork}", + "Policies": [], + "MacAddress" : {MacAddress} }})"; // Render the template - const auto endpoint_settings = fmt::format(endpoint_settings_template, string_to_wstring(params.network_guid)); + const auto endpoint_settings = fmt::format( + endpoint_settings_template, + fmt::arg(L"HostComputeNetwork", string_to_wstring(params.network_guid)), + fmt::arg(L"MacAddress", + params.mac_address ? fmt::format(L"\"{}\"", string_to_wstring(params.mac_address.value())) : L"null")); HCN_ENDPOINT endpoint{nullptr}; const auto result = perform_hcn_operation(api, api.CreateEndpoint, diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h index 2ad5030a80..9feb8a1183 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h @@ -19,6 +19,8 @@ #define MULTIPASS_HYPERV_API_HCN_CREATE_ENDPOINT_PARAMETERS_H #include + +#include #include namespace multipass::hyperv::hcn @@ -42,6 +44,14 @@ struct CreateEndpointParameters * Must be unique. */ std::string endpoint_guid{}; + + /** + * MAC address assocaited with the endpoint (optional). + * + * HCN will auto-assign a MAC address to the endpoint when + * not specified, where applicable. + */ + std::optional mac_address; }; } // namespace multipass::hyperv::hcn @@ -61,9 +71,10 @@ struct fmt::formatter auto format(const multipass::hyperv::hcn::CreateEndpointParameters& params, FormatContext& ctx) const { return format_to(ctx.out(), - "Endpoint GUID: ({}) | Network GUID: ({})", + "Endpoint GUID: ({}) | Network GUID: ({}) | MAC address: ({})", params.endpoint_guid, - params.network_guid); + params.network_guid, + params.mac_address); } }; diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index bc18ffa052..aa1230163b 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -440,7 +440,8 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) }, "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ - ] + ], + "MacAddress": null })"""; ASSERT_NE(nullptr, network); @@ -590,7 +591,8 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) }, "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ - ] + ], + "MacAddress": null })"""; ASSERT_EQ(mock_network_object, network); From e61bdb2d55c34a758608314612f22430ebae5a38 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 21:32:47 +0300 Subject: [PATCH 30/76] [hyperv-hcn-vm] simplify endpoint create/add parameters setup --- .../hyperv_api/hcs_virtual_machine.cpp | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index c5607b4c0d..ec7e82da90 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -213,20 +213,31 @@ bool HCSVirtualMachine::maybe_create_compute_system() // FIXME: Handle suspend state? - const auto endpoint_params = [this]() { + const auto create_endpoint_params = [this]() { std::vector endpoint_params{}; - endpoint_params.emplace_back( - hcn::CreateEndpointParameters{primary_network_guid, mac2uuid(description.default_mac_address)}); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // endpoint_params.emplace_back(network_guid, mac2uuid(v.mac_address)); - // } + + // The primary endpoint (management) + hcn::CreateEndpointParameters primary_endpoint{}; + primary_endpoint.network_guid = primary_network_guid; + primary_endpoint.endpoint_guid = mac2uuid(description.default_mac_address); + primary_endpoint.mac_address = description.default_mac_address; + replace_colon_with_dash(primary_endpoint.mac_address.value()); + endpoint_params.push_back(primary_endpoint); + + // Additional endpoints, a.k.a. extra interfaces. + for (const auto& v : description.extra_interfaces) + { + hcn::CreateEndpointParameters extra_endpoint{}; + extra_endpoint.network_guid = multipass::utils::make_uuid(v.id).toStdString(); + extra_endpoint.endpoint_guid = mac2uuid(v.mac_address); + extra_endpoint.mac_address = v.mac_address; + replace_colon_with_dash(extra_endpoint.mac_address.value()); + endpoint_params.push_back(extra_endpoint); + } return endpoint_params; }(); - for (const auto& endpoint : endpoint_params) + for (const auto& endpoint : create_endpoint_params) { // There might be remnants from an old VM, remove the endpoint if exist before // creating it again. @@ -245,7 +256,7 @@ bool HCSVirtualMachine::maybe_create_compute_system() // E_INVALIDARG means there's no such VM. // Create the VM from scratch. - const auto ccs_params = [this, &endpoint_params]() { + const auto ccs_params = [this, &create_endpoint_params]() { hcs::CreateComputeSystemParameters ccs_params{}; ccs_params.name = description.vm_name; ccs_params.memory_size_mb = description.mem_size.in_megabytes(); @@ -253,27 +264,21 @@ bool HCSVirtualMachine::maybe_create_compute_system() ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); ccs_params.vhdx_path = description.image.image_path.toStdString(); - hcs::AddEndpointParameters default_endpoint_params{}; - default_endpoint_params.nic_mac_address = description.default_mac_address; - // Hyper-V API does not like colons. Ensure that the MAC is separated - // with dash instead of colon. - replace_colon_with_dash(default_endpoint_params.nic_mac_address); - // make the UUID deterministic so we can query the endpoint with a MAC address - // if needed. - default_endpoint_params.endpoint_guid = mac2uuid(description.default_mac_address); - default_endpoint_params.target_compute_system_name = ccs_params.name; - ccs_params.endpoints.push_back(default_endpoint_params); - - // TODO: Figure out what to do with the "extra interfaces" - // for (const auto& v : desc.extra_interfaces) - // { - // hcs::AddEndpointParameters endpoint_params{}; - // endpoint_params.nic_mac_address = v.mac_address; - // endpoint_params.endpoint_guid = mac2uuid(v.mac_address); - // endpoint_params.target_compute_system_name = ccs_params.name; - // ccs_params.endpoints.push_back(endpoint_params); - // } - + static auto create_to_add = [this](const auto& create_params){ + hcs::AddEndpointParameters add_params{}; + add_params.endpoint_guid = create_params.endpoint_guid; + if(!create_params.mac_address){ + throw CreateEndpointException("One of the endpoints do not have a MAC address!"); + } + add_params.nic_mac_address = create_params.mac_address.value(); + add_params.target_compute_system_name = vm_name; + return add_params; + }; + + std::transform(create_endpoint_params.begin(), + create_endpoint_params.end(), + std::back_inserter(ccs_params.endpoints), + create_to_add); return ccs_params; }(); From 81ff2b6aad98acfbf0a34ff86bc291d9587bf2bf Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 15 Apr 2025 21:40:00 +0300 Subject: [PATCH 31/76] [hyperv-hcs] make the linter happy --- src/platform/backends/hyperv_api/hcs_virtual_machine.cpp | 6 +++--- .../backends/hyperv_api/hcs_virtual_machine_factory.h | 2 +- 2 files changed, 4 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 ec7e82da90..647a76e8ed 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -31,7 +31,6 @@ #include #include - #include #include @@ -264,10 +263,11 @@ bool HCSVirtualMachine::maybe_create_compute_system() ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); ccs_params.vhdx_path = description.image.image_path.toStdString(); - static auto create_to_add = [this](const auto& create_params){ + static auto create_to_add = [this](const auto& create_params) { hcs::AddEndpointParameters add_params{}; add_params.endpoint_guid = create_params.endpoint_guid; - if(!create_params.mac_address){ + if (!create_params.mac_address) + { throw CreateEndpointException("One of the endpoints do not have a MAC address!"); } add_params.nic_mac_address = create_params.mac_address.value(); diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h index ff00fac63d..6836115a27 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -50,7 +50,7 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory }; std::vector networks() const override; - + void require_snapshots_support() const override { } From 4e3b70ac80fb4d75c89c45f87466ec3a74096016 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 16 Apr 2025 20:52:12 +0300 Subject: [PATCH 32/76] [hyperv-hcn] unify formatting for HCN data structures Formatting HCN data types with fmt will now yield their JSON representation for both `char` and `wchar_t`. - Added bitwise | operator for HcnNetworkFlags - Moved most of the HCN formatter implementations to source - Added `universal_string_literal_helper` and `maybe_widen` helpers - Added unit tests for format results - Added more tests for create_network --- .../backends/hyperv_api/CMakeLists.txt | 4 + .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 127 +------ .../hyperv_api/hcn/hyperv_hcn_ipam.cpp | 53 +++ .../backends/hyperv_api/hcn/hyperv_hcn_ipam.h | 16 +- .../hyperv_api/hcn/hyperv_hcn_network_flags.h | 13 + .../hcn/hyperv_hcn_network_policy.cpp | 64 ++++ .../hcn/hyperv_hcn_network_policy.h | 22 +- .../hyperv_api/hcn/hyperv_hcn_route.cpp | 50 +++ .../hyperv_api/hcn/hyperv_hcn_route.h | 22 +- .../hyperv_api/hcn/hyperv_hcn_subnet.cpp | 55 +++ .../hyperv_api/hcn/hyperv_hcn_subnet.h | 19 +- .../hyperv_api/hyperv_api_string_conversion.h | 100 ++++++ tests/hyperv_api/CMakeLists.txt | 4 + tests/hyperv_api/hyperv_test_utils.h | 5 +- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 335 ++++++++++++++++-- tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp | 108 ++++++ .../test_ut_hyperv_hcn_network_policy.cpp | 84 +++++ tests/hyperv_api/test_ut_hyperv_hcn_route.cpp | 73 ++++ .../hyperv_api/test_ut_hyperv_hcn_subnet.cpp | 96 +++++ 19 files changed, 1035 insertions(+), 215 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp create mode 100644 src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp create mode 100644 src/platform/backends/hyperv_api/hyperv_api_string_conversion.h create mode 100644 tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp create mode 100644 tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp create mode 100644 tests/hyperv_api/test_ut_hyperv_hcn_route.cpp create mode 100644 tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 4241879b43..0031a0ec5a 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -43,6 +43,10 @@ if(WIN32) add_library(hyperv_api_backend STATIC hyperv_api_common.cpp hcn/hyperv_hcn_api_wrapper.cpp + hcn/hyperv_hcn_route.cpp + hcn/hyperv_hcn_subnet.cpp + hcn/hyperv_hcn_ipam.cpp + hcn/hyperv_hcn_network_policy.cpp hcs/hyperv_hcs_api_wrapper.cpp virtdisk/virtdisk_api_wrapper.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 6fc5b54a0d..97a192361c 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -154,120 +154,6 @@ auto hcn_errc_to_log_level(const OperationResult& result) return mpl::Level::error; } -/** - * For each element in @p elem, apply @p op and - * aggregate the result. Then, join all by comma. - * - * @tparam T Element type - * @tparam Func Function type - * @param [in] elems Elements - * @param [in] op Operation to apply - * @return Comma-separated list of transformed elements - */ -template -auto transform_join(const std::vector& elems, Func op) -{ - std::vector result; - std::transform(elems.begin(), elems.end(), std::back_inserter(result), op); - return fmt::format(L"{}", fmt::join(result, L",")); -} - -/** - * Format a HcnRoute - * - * @param [in] route Route to format - * @return HCN JSON representation of @p route - */ -std::wstring format_route(const HcnRoute& route) -{ - constexpr auto route_template = LR"""( - {{ - "NextHop": "{NextHop}", - "DestinationPrefix": "{DestinationPrefix}", - "Metric": {Metric} - }} - )"""; - - return fmt::format(route_template, - fmt::arg(L"NextHop", string_to_wstring(route.next_hop)), - fmt::arg(L"DestinationPrefix", string_to_wstring(route.destination_prefix)), - fmt::arg(L"Metric", route.metric)); -} - -/** - * Format a HcnSubnet - * - * @param [in] subnet Subnet to format - * @return HCN JSON representation of @p subnet - */ -std::wstring format_subnet(const HcnSubnet& subnet) -{ - constexpr auto subnet_template = LR"""( - {{ - "Policies": [], - "Routes" : [ - {Routes} - ], - "IpAddressPrefix" : "{IpAddressPrefix}", - "IpSubnets": null - }} - )"""; - - return fmt::format(subnet_template, - fmt::arg(L"IpAddressPrefix", string_to_wstring(subnet.ip_address_prefix)), - fmt::arg(L"Routes", transform_join(subnet.routes, format_route))); -} - -/** - * Format a HcnIpam - * - * @param [in] ipam IPAM to format - * @return HCN JSON representation of @p ipam - */ -std::wstring format_ipam(const HcnIpam& ipam) -{ - constexpr auto ipam_template = LR"""( - {{ - "Type": "{Type}", - "Subnets": [ - {Subnets} - ] - }} - )"""; - - return fmt::format(ipam_template, - fmt::arg(L"Type", string_to_wstring(std::string{ipam.type})), - fmt::arg(L"Subnets", transform_join(ipam.subnets, format_subnet))); -} - -struct NetworkPolicySettingsFormatters -{ - std::wstring operator()(const HcnNetworkPolicyNetAdapterName& policy) - { - constexpr auto netadaptername_settings_template = LR"""( - "NetworkAdapterName": "{NetworkAdapterName}" - )"""; - - return fmt::format(netadaptername_settings_template, - fmt::arg(L"NetworkAdapterName", string_to_wstring(policy.net_adapter_name))); - } -}; - -std::wstring format_network_policy(const HcnNetworkPolicy& policy) -{ - constexpr auto network_policy_template = LR"""( - {{ - "Type": "{Type}", - "Settings": {{ - {Settings} - }} - }} - )"""; - return fmt::format(network_policy_template, - fmt::arg(L"Type", string_to_wstring(policy.type)), - fmt::arg(L"Settings", std::visit(NetworkPolicySettingsFormatters{}, policy.settings))); -} - } // namespace // --------------------------------------------------------- @@ -306,13 +192,12 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params )"""; // Render the template - const auto network_settings = - fmt::format(network_settings_template, - fmt::arg(L"Name", string_to_wstring(params.name)), - fmt::arg(L"Type", string_to_wstring(std::string{params.type})), - fmt::arg(L"Flags", fmt::underlying(params.flags)), - fmt::arg(L"Ipams", transform_join(params.ipams, format_ipam)), - fmt::arg(L"Policies", transform_join(params.policies, format_network_policy))); + const auto network_settings = fmt::format(network_settings_template, + fmt::arg(L"Name", string_to_wstring(params.name)), + fmt::arg(L"Type", string_to_wstring(std::string{params.type})), + fmt::arg(L"Flags", fmt::underlying(params.flags)), + fmt::arg(L"Ipams", fmt::join(params.ipams, L",")), + fmt::arg(L"Policies", fmt::join(params.policies, L","))); HCN_NETWORK network{nullptr}; const auto result = perform_hcn_operation(api, diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp new file mode 100644 index 0000000000..c146246fb5 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +namespace hv = multipass::hyperv; +namespace hcn = hv::hcn; + +template +template +auto fmt::formatter::format(const hcn::HcnIpam& ipam, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto subnet_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "Type": "{}", + "Subnets": [ + {} + ] + }} + )json"); + + constexpr static auto comma = MULTIPASS_UNIVERSAL_LITERAL(","); + + return format_to(ctx.out(), + subnet_template.as(), + hv::maybe_widen{ipam.type}, + fmt::join(ipam.subnets, comma.as())); +} + +template auto fmt::formatter::format(const hcn::HcnIpam&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const hcn::HcnIpam&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h index e30e538297..57dc8c462d 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h @@ -45,21 +45,11 @@ struct HcnIpam * Formatter type specialization for HcnIpam */ template -struct fmt::formatter +struct fmt::formatter : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcn::HcnIpam& ipam, FormatContext& ctx) const - { - return format_to(ctx.out(), - "Type: ({}) | Subnets: ({})", - static_cast(ipam.type), - fmt::join(ipam.subnets, ",")); - } + auto format(const multipass::hyperv::hcn::HcnIpam& ipam, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h index 5ed952b2cf..2303491829 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h @@ -46,6 +46,19 @@ enum class HcnNetworkFlags : std::uint32_t enable_iov = 1 << 13, ///< ?? }; +inline HcnNetworkFlags operator|(HcnNetworkFlags lhs, HcnNetworkFlags rhs) noexcept +{ + using U = std::underlying_type_t; + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +inline HcnNetworkFlags& operator|=(HcnNetworkFlags& lhs, HcnNetworkFlags rhs) noexcept +{ + using U = std::underlying_type_t; + lhs = (lhs | rhs); + return lhs; +} + } // namespace multipass::hyperv::hcn /** diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp new file mode 100644 index 0000000000..f84785e305 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include + +namespace hv = multipass::hyperv; +namespace hcn = hv::hcn; + +template +struct NetworkPolicySettingsFormatters +{ + auto operator()(const hcn::HcnNetworkPolicyNetAdapterName& policy) + { + constexpr static auto netadaptername_settings_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + "NetworkAdapterName": "{}" + )json"); + + return fmt::format(netadaptername_settings_template.as(), hv::maybe_widen{policy.net_adapter_name}); + } +}; + +template +template +auto fmt::formatter::format(const hcn::HcnNetworkPolicy& policy, FormatContext& ctx) const + -> typename FormatContext::iterator +{ + constexpr static auto route_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "Type": "{}", + "Settings": {{ + {} + }} + }} + )json"); + + return format_to(ctx.out(), + route_template.as(), + hv::maybe_widen{policy.type}, + std::visit(NetworkPolicySettingsFormatters{}, policy.settings)); +} + +template auto fmt::formatter::format(const hcn::HcnNetworkPolicy&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const hcn::HcnNetworkPolicy&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h index 38fba8cd28..5a605f0eb8 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h @@ -21,6 +21,8 @@ #include #include +#include + #include namespace multipass::hyperv::hcn @@ -47,25 +49,11 @@ struct HcnNetworkPolicy * Formatter type specialization for HcnNetworkPolicy */ template -struct fmt::formatter +struct fmt::formatter : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcn::HcnNetworkPolicy& params, FormatContext& ctx) const - { - return std::visit( - [&](const auto& settings) { - return fmt::format_to(ctx.out(), - "Type: {} | Settings: {}", - static_cast(params.type), - settings); - }, - params.settings); - } + auto format(const multipass::hyperv::hcn::HcnNetworkPolicy& policy, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp new file mode 100644 index 0000000000..c7f7211641 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +namespace hv = multipass::hyperv; +namespace hcn = hv::hcn; + +template +template +auto fmt::formatter::format(const hcn::HcnRoute& route, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto route_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "NextHop": "{}", + "DestinationPrefix": "{}", + "Metric": {} + }})json"); + + return format_to(ctx.out(), + route_template.as(), + hv::maybe_widen{route.next_hop}, + hv::maybe_widen(route.destination_prefix), + route.metric); +} + +template auto fmt::formatter::format(const hcn::HcnRoute&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const hcn::HcnRoute&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h index 6e6978c2f4..9eff7b4860 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h @@ -18,10 +18,9 @@ #ifndef MULTIPASS_HYPERV_API_HCN_ROUTE_H #define MULTIPASS_HYPERV_API_HCN_ROUTE_H -#include +#include -#include -#include +#include namespace multipass::hyperv::hcn { @@ -48,22 +47,11 @@ struct HcnRoute * Formatter type specialization for HcnRoute */ template -struct fmt::formatter +struct fmt::formatter : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcn::HcnRoute& route, FormatContext& ctx) const - { - return format_to(ctx.out(), - "Next Hop: ({}) | Destination Prefix: ({}) | Metric: ({})", - route.next_hop, - route.destination_prefix, - route.metric); - } + auto format(const multipass::hyperv::hcn::HcnRoute& route, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp new file mode 100644 index 0000000000..5dc039d5c1 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +namespace hv = multipass::hyperv; +namespace hcn = hv::hcn; + +template +template +auto fmt::formatter::format(const hcn::HcnSubnet& subnet, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto subnet_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "Policies": [], + "Routes" : [ + {} + ], + "IpAddressPrefix" : "{}", + "IpSubnets": null + }} + )json"); + + constexpr static auto comma = MULTIPASS_UNIVERSAL_LITERAL(","); + + return format_to(ctx.out(), + subnet_template.as(), + fmt::join(subnet.routes, comma.as()), + hv::maybe_widen{subnet.ip_address_prefix}); +} + +template auto fmt::formatter::format(const hcn::HcnSubnet&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const hcn::HcnSubnet&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h index 28e9458deb..b718985781 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h @@ -20,8 +20,7 @@ #include -#include -#include +#include #include #include @@ -41,21 +40,11 @@ struct HcnSubnet * Formatter type specialization for HcnSubnet */ template -struct fmt::formatter +struct fmt::formatter : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcn::HcnSubnet& subnet, FormatContext& ctx) const - { - return format_to(ctx.out(), - "IP Address Prefix: ({}) | Routes: ({})", - subnet.ip_address_prefix, - fmt::join(subnet.routes, ",")); - } + auto format(const multipass::hyperv::hcn::HcnSubnet& route, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif diff --git a/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h new file mode 100644 index 0000000000..f02604b21c --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h @@ -0,0 +1,100 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_STRING_CONVERSION_H +#define MULTIPASS_HYPERV_API_STRING_CONVERSION_H + +#include +#include +#include +#include + +#include + +namespace multipass::hyperv +{ + +struct universal_string_literal_helper +{ + std::string_view narrow; + std::wstring_view wide; + + template + auto as() const; + + template <> + auto as() const + { + return narrow; + } + + template <> + auto as() const + { + return wide; + } +}; + +struct maybe_widen +{ + explicit maybe_widen(const std::string& v) : narrow(v) + { + } + + operator const std::string&() const + { + return narrow; + } + + operator std::wstring() const + { + return std::wstring_convert>().from_bytes(narrow); + } + +private: + const std::string& narrow; +}; + +} // namespace multipass::hyperv + +/** + * Formatter type specialization for CreateNetworkParameters + */ +template +struct fmt::formatter +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } + + template + auto format(const multipass::hyperv::maybe_widen& params, FormatContext& ctx) const + { + constexpr static Char fmt_str[] = {'{', '}', '\0'}; + using const_sref_type = const std::basic_string&; + return format_to(ctx.out(), fmt_str, static_cast(params)); + } +}; + +#define MULTIPASS_UNIVERSAL_LITERAL(X) \ + multipass::hyperv::universal_string_literal_helper \ + { \ + "" X, L"" X \ + } + +#endif // MULTIPASS_HYPERV_API_STRING_CONVERSION_H diff --git a/tests/hyperv_api/CMakeLists.txt b/tests/hyperv_api/CMakeLists.txt index c55bc03c34..bf350964cc 100644 --- a/tests/hyperv_api/CMakeLists.txt +++ b/tests/hyperv_api/CMakeLists.txt @@ -19,6 +19,10 @@ if(WIN32) PRIVATE ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_hcn_api.cpp ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_api.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_route.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_subnet.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_ipam.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_network_policy.cpp ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_hcs_api.cpp ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_api.cpp ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_virtdisk.cpp diff --git a/tests/hyperv_api/hyperv_test_utils.h b/tests/hyperv_api/hyperv_test_utils.h index f301aba5e1..117cf3ea4d 100644 --- a/tests/hyperv_api/hyperv_test_utils.h +++ b/tests/hyperv_api/hyperv_test_utils.h @@ -26,9 +26,10 @@ namespace multipass::test { -inline auto trim_whitespace(const wchar_t* input) +template +inline auto trim_whitespace(const CharT* input) { - std::wstring str{input}; + std::basic_string str{input}; str.erase(std::remove_if(str.begin(), str.end(), ::iswspace), str.end()); return str; } diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index aa1230163b..c36f14ddf1 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -15,22 +15,25 @@ * */ -#include "hyperv_api/hcn/hyperv_hcn_api_table.h" #include "hyperv_test_utils.h" +#include "tests/common.h" #include "tests/mock_logger.h" -#include "gmock/gmock.h" +#include +#include +#include +#include +#include + +#include + #include #include -#include -#include -#include -#include -#include #include namespace mpt = multipass::test; namespace mpl = multipass::logging; +namespace hcn = multipass::hyperv::hcn; using testing::DoAll; using testing::Return; @@ -38,7 +41,7 @@ using testing::Return; namespace multipass::test { -using uut_t = hyperv::hcn::HCNWrapper; +using uut_t = hcn::HCNWrapper; struct HyperVHCNAPI_UnitTests : public ::testing::Test { @@ -86,15 +89,15 @@ struct HyperVHCNAPI_UnitTests : public ::testing::Test // Initialize the API table with stub functions, so if any of these fire without // our will, we'll know. - hyperv::hcn::HCNAPITable mock_api_table{stub_mock_create_network.AsStdFunction(), - stub_mock_open_network.AsStdFunction(), - stub_mock_delete_network.AsStdFunction(), - stub_mock_close_network.AsStdFunction(), - stub_mock_create_endpoint.AsStdFunction(), - stub_mock_open_endpoint.AsStdFunction(), - stub_mock_delete_endpoint.AsStdFunction(), - stub_mock_close_endpoint.AsStdFunction(), - stub_mock_cotaskmemfree.AsStdFunction()}; + hcn::HCNAPITable mock_api_table{stub_mock_create_network.AsStdFunction(), + stub_mock_open_network.AsStdFunction(), + stub_mock_delete_network.AsStdFunction(), + stub_mock_close_network.AsStdFunction(), + stub_mock_create_endpoint.AsStdFunction(), + stub_mock_open_endpoint.AsStdFunction(), + stub_mock_delete_endpoint.AsStdFunction(), + stub_mock_close_endpoint.AsStdFunction(), + stub_mock_cotaskmemfree.AsStdFunction()}; // Sentinel values as mock API parameters. These handles are opaque handles and // they're not being dereferenced in any way -- only address values are compared. @@ -111,7 +114,7 @@ struct HyperVHCNAPI_UnitTests : public ::testing::Test /** * Success scenario: Everything goes as expected. */ -TEST_F(HyperVHCNAPI_UnitTests, create_network_success) +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_ics) { /****************************************************** * Override the default mock functions. @@ -177,11 +180,285 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success) ******************************************************/ { uut_t uut{mock_api_table}; - hyperv::hcn::CreateNetworkParameters params{}; + hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.ipams = { - hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; + params.ipams = {hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; + + const auto& [status, status_msg] = uut.create_network(params); + ASSERT_TRUE(status); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_transparent) +{ + /****************************************************** + * Override the default mock functions. + ******************************************************/ + ::testing::MockFunction mock_create_network; + ::testing::MockFunction mock_close_network; + + mock_api_table.CreateNetwork = mock_create_network.AsStdFunction(); + mock_api_table.CloseNetwork = mock_close_network.AsStdFunction(); + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + EXPECT_CALL(mock_create_network, Call) + .WillOnce(DoAll( + [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { + constexpr auto expected_network_settings = LR"""( + { + "SchemaVersion": { + "Major": 2, + "Minor": 2 + }, + "Name": "multipass-hyperv-api-hcn-create-test", + "Type": "Transparent", + "Ipams": [ + ], + "Flags" : 0, + "Policies": [ + { + "Type": "NetAdapterName", + "Settings": + { + "NetworkAdapterName": "test adapter" + } + } + ] + } + )"""; + ASSERT_NE(nullptr, network); + ASSERT_EQ(nullptr, *network); + ASSERT_NE(nullptr, error_record); + ASSERT_EQ(nullptr, *error_record); + const auto config_no_whitespace = trim_whitespace(settings); + const auto expected_no_whitespace = trim_whitespace(expected_network_settings); + ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); + const auto guid_str = hyperv::guid_to_string(id); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + *network = mock_network_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_close_network, Call) + .WillOnce(DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + /****************************************************** + * Verify the expected outcome. + ******************************************************/ + { + uut_t uut{mock_api_table}; + hcn::CreateNetworkParameters params{}; + params.type = hcn::HcnNetworkType::Transparent(); + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = {}; + hcn::HcnNetworkPolicy policy{hcn::HcnNetworkPolicyType::NetAdapterName(), + hcn::HcnNetworkPolicyNetAdapterName{"test adapter"}}; + params.policies.push_back(policy); + + const auto& [status, status_msg] = uut.create_network(params); + ASSERT_TRUE(status); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_with_flags_multiple_policies) +{ + /****************************************************** + * Override the default mock functions. + ******************************************************/ + ::testing::MockFunction mock_create_network; + ::testing::MockFunction mock_close_network; + + mock_api_table.CreateNetwork = mock_create_network.AsStdFunction(); + mock_api_table.CloseNetwork = mock_close_network.AsStdFunction(); + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + EXPECT_CALL(mock_create_network, Call) + .WillOnce(DoAll( + [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { + constexpr auto expected_network_settings = LR"""( + { + "SchemaVersion": { + "Major": 2, + "Minor": 2 + }, + "Name": "multipass-hyperv-api-hcn-create-test", + "Type": "Transparent", + "Ipams": [ + ], + "Flags" : 10, + "Policies": [ + { + "Type": "NetAdapterName", + "Settings": + { + "NetworkAdapterName": "test adapter" + } + }, + { + "Type": "NetAdapterName", + "Settings": + { + "NetworkAdapterName": "test adapter" + } + } + ] + } + )"""; + ASSERT_NE(nullptr, network); + ASSERT_EQ(nullptr, *network); + ASSERT_NE(nullptr, error_record); + ASSERT_EQ(nullptr, *error_record); + const auto config_no_whitespace = trim_whitespace(settings); + const auto expected_no_whitespace = trim_whitespace(expected_network_settings); + ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); + const auto guid_str = hyperv::guid_to_string(id); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + *network = mock_network_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_close_network, Call) + .WillOnce(DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + /****************************************************** + * Verify the expected outcome. + ******************************************************/ + { + uut_t uut{mock_api_table}; + hcn::CreateNetworkParameters params{}; + params.type = hcn::HcnNetworkType::Transparent(); + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = {}; + params.flags = hcn::HcnNetworkFlags::enable_dhcp_server | hcn::HcnNetworkFlags::enable_non_persistent; + hcn::HcnNetworkPolicy policy{hcn::HcnNetworkPolicyType::NetAdapterName(), + hcn::HcnNetworkPolicyNetAdapterName{"test adapter"}}; + params.policies.push_back(policy); + params.policies.push_back(policy); + + const auto& [status, status_msg] = uut.create_network(params); + ASSERT_TRUE(status); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_multiple_ipams) +{ + /****************************************************** + * Override the default mock functions. + ******************************************************/ + ::testing::MockFunction mock_create_network; + ::testing::MockFunction mock_close_network; + + mock_api_table.CreateNetwork = mock_create_network.AsStdFunction(); + mock_api_table.CloseNetwork = mock_close_network.AsStdFunction(); + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + EXPECT_CALL(mock_create_network, Call) + .WillOnce(DoAll( + [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { + constexpr auto expected_network_settings = LR"""( + { + "SchemaVersion": { + "Major": 2, + "Minor": 2 + }, + "Name": "multipass-hyperv-api-hcn-create-test", + "Type": "Transparent", + "Ipams": [ + { + "Type": "static", + "Subnets": [ + { + "Policies": [], + "Routes": [ + { + "NextHop": "10.0.0.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 0 + } + ], + "IpAddressPrefix": "10.0.0.10/10", + "IpSubnets": null + } + ] + }, + { + "Type": "DHCP", + "Subnets": [] + } + ], + "Flags" : 0, + "Policies": [] + } + )"""; + ASSERT_NE(nullptr, network); + ASSERT_EQ(nullptr, *network); + ASSERT_NE(nullptr, error_record); + ASSERT_EQ(nullptr, *error_record); + const auto config_no_whitespace = trim_whitespace(settings); + const auto expected_no_whitespace = trim_whitespace(expected_network_settings); + ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); + const auto guid_str = hyperv::guid_to_string(id); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + *network = mock_network_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_close_network, Call) + .WillOnce(DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + /****************************************************** + * Verify the expected outcome. + ******************************************************/ + { + uut_t uut{mock_api_table}; + hcn::CreateNetworkParameters params{}; + params.type = hcn::HcnNetworkType::Transparent(); + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + hcn::HcnIpam ipam1; + ipam1.type = hcn::HcnIpamType::Static(); + ipam1.subnets.push_back(hcn::HcnSubnet{"10.0.0.10/10", {hcn::HcnRoute{"10.0.0.1", "0.0.0.0/0", 0}}}); + hcn::HcnIpam ipam2; + ipam2.type = hcn::HcnIpamType::Dhcp(); + + params.ipams.push_back(ipam1); + params.ipams.push_back(ipam2); const auto& [status, status_msg] = uut.create_network(params); ASSERT_TRUE(status); @@ -229,11 +506,10 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_close_network_failed) * Verify the expected outcome. ******************************************************/ { - hyperv::hcn::CreateNetworkParameters params{}; + hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.ipams = { - hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; + params.ipams = {hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; uut_t uut{mock_api_table}; const auto& [success, error_msg] = uut.create_network(params); @@ -290,11 +566,10 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_failed) * Verify the expected outcome. ******************************************************/ { - hyperv::hcn::CreateNetworkParameters params{}; + hcn::CreateNetworkParameters params{}; params.name = "multipass-hyperv-api-hcn-create-test"; params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; - params.ipams = { - hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"172.50.224.0/20"}}}}; + params.ipams = {hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; uut_t uut{mock_api_table}; const auto& [success, error_msg] = uut.create_network(params); @@ -495,7 +770,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) ******************************************************/ { uut_t uut{mock_api_table}; - hyperv::hcn::CreateEndpointParameters params{}; + hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; @@ -543,7 +818,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_open_network_failed) ******************************************************/ { uut_t uut{mock_api_table}; - hyperv::hcn::CreateEndpointParameters params{}; + hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; @@ -642,7 +917,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) ******************************************************/ { uut_t uut{mock_api_table}; - hyperv::hcn::CreateEndpointParameters params{}; + hcn::CreateEndpointParameters params{}; params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp new file mode 100644 index 0000000000..5e0b37fb6c --- /dev/null +++ b/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "tests/common.h" +#include "tests/hyperv_api/hyperv_test_utils.h" + +#include + +namespace hcn = multipass::hyperv::hcn; + +namespace multipass::test +{ + +using uut_t = hcn::HcnIpam; + +struct HyperVHCNIpam_UnitTests : public ::testing::Test +{ +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNIpam_UnitTests, format_narrow) +{ + uut_t uut; + uut.type = hyperv::hcn::HcnIpamType::Static(); + uut.subnets.emplace_back( + hyperv::hcn::HcnSubnet{"192.168.1.0/24", {hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}}}); + const auto result = fmt::format("{}", uut); + constexpr auto expected_result = R"json( + { + "Type": "static", + "Subnets": [ + { + "Policies": [], + "Routes" : [ + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + } + ], + "IpAddressPrefix" : "192.168.1.0/24", + "IpSubnets": null + } + ] + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNIpam_UnitTests, format_wide) +{ + uut_t uut; + uut.type = hyperv::hcn::HcnIpamType::Dhcp(); + uut.subnets.emplace_back( + hyperv::hcn::HcnSubnet{"192.168.1.0/24", {hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}}}); + const auto result = fmt::format("{}", uut); + constexpr auto expected_result = R"json( + { + "Type": "DHCP", + "Subnets": [ + { + "Policies": [], + "Routes" : [ + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + } + ], + "IpAddressPrefix" : "192.168.1.0/24", + "IpSubnets": null + } + ] + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +} // namespace multipass::test diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp new file mode 100644 index 0000000000..77e4b3ed2a --- /dev/null +++ b/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "tests/common.h" +#include "tests/hyperv_api/hyperv_test_utils.h" + +#include + +namespace hcn = multipass::hyperv::hcn; + +namespace multipass::test +{ + +using uut_t = hcn::HcnNetworkPolicy; + +struct HyperVHCNNetworkPolicy_UnitTests : public ::testing::Test +{ +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNNetworkPolicy_UnitTests, format_narrow) +{ + uut_t uut{hyperv::hcn::HcnNetworkPolicyType::NetAdapterName()}; + uut.settings = hyperv::hcn::HcnNetworkPolicyNetAdapterName{"client eastwood"}; + + const auto result = fmt::format("{}", uut); + constexpr auto expected_result = R"json( + { + "Type": "NetAdapterName", + "Settings": { + "NetworkAdapterName": "client eastwood" + } + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNNetworkPolicy_UnitTests, format_wide) +{ + uut_t uut{hyperv::hcn::HcnNetworkPolicyType::NetAdapterName()}; + uut.settings = hyperv::hcn::HcnNetworkPolicyNetAdapterName{"client eastwood"}; + + const auto result = fmt::format(L"{}", uut); + constexpr auto expected_result = LR"json( + { + "Type": "NetAdapterName", + "Settings": { + "NetworkAdapterName": "client eastwood" + } + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +} // namespace multipass::test diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp new file mode 100644 index 0000000000..4c3cda8c4f --- /dev/null +++ b/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "tests/common.h" + +#include + +namespace hcn = multipass::hyperv::hcn; + +namespace multipass::test +{ + +using uut_t = hcn::HcnRoute; + +struct HyperVHCNRoute_UnitTests : public ::testing::Test +{ +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNRoute_UnitTests, format_narrow) +{ + uut_t uut; + uut.destination_prefix = "0.0.0.0/0"; + uut.metric = 123; + uut.next_hop = "192.168.1.1"; + const auto result = fmt::format("{}", uut); + constexpr auto expected_result = R"json( + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + })json"; + EXPECT_STREQ(result.c_str(), expected_result); +} + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNRoute_UnitTests, format_wide) +{ + uut_t uut; + uut.destination_prefix = "0.0.0.0/0"; + uut.metric = 123; + uut.next_hop = "192.168.1.1"; + const auto result = fmt::format(L"{}", uut); + constexpr auto expected_result = LR"json( + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + })json"; + EXPECT_STREQ(result.c_str(), expected_result); +} + +} // namespace multipass::test diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp new file mode 100644 index 0000000000..55e9491815 --- /dev/null +++ b/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "tests/common.h" +#include "tests/hyperv_api/hyperv_test_utils.h" + +#include + +namespace hcn = multipass::hyperv::hcn; + +namespace multipass::test +{ + +using uut_t = hcn::HcnSubnet; + +struct HyperVHCNSubnet_UnitTests : public ::testing::Test +{ +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNSubnet_UnitTests, format_narrow) +{ + uut_t uut; + uut.ip_address_prefix = "192.168.1.0/24"; + uut.routes.emplace_back(hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}); + const auto result = fmt::format("{}", uut); + constexpr auto expected_result = R"json( + { + "Policies": [], + "Routes" : [ + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + } + ], + "IpAddressPrefix" : "192.168.1.0/24", + "IpSubnets": null + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNSubnet_UnitTests, format_wide) +{ + uut_t uut; + uut.ip_address_prefix = "192.168.1.0/24"; + uut.routes.emplace_back(hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}); + const auto result = fmt::format(L"{}", uut); + constexpr auto expected_result = LR"json( + { + "Policies": [], + "Routes" : [ + { + "NextHop": "192.168.1.1", + "DestinationPrefix": "0.0.0.0/0", + "Metric": 123 + } + ], + "IpAddressPrefix" : "192.168.1.0/24", + "IpSubnets": null + })json"; + + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(expected_result); + + EXPECT_STREQ(result_nws.c_str(), expected_nws.c_str()); +} + +} // namespace multipass::test From 6c1d221263a6b3a269c4cae827e9adc829c187d6 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 16 Apr 2025 22:01:41 +0300 Subject: [PATCH 33/76] [hyperv-api] efforts to make the formatter happy --- include/multipass/virtual_machine.h | 2 +- .../hcn/hyperv_hcn_network_policy_type.h | 3 ++- .../hyperv_hcs_create_compute_system_params.h | 2 +- .../hyperv_api/hcs_virtual_machine.cpp | 4 +++- src/platform/platform_win.cpp | 23 ++++++++++++------- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 16d70cba75..5a643c719a 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -138,7 +138,7 @@ class VirtualMachine : private DisabledCopyMove VirtualMachine(VirtualMachine::State state, const std::string& vm_name, const Path& instance_dir) : state{state}, vm_name{vm_name}, instance_dir{QDir{instance_dir}} {}; VirtualMachine(const std::string& vm_name, const Path& instance_dir) - : VirtualMachine(State::off, vm_name, instance_dir) {}; + : VirtualMachine(State::off, vm_name, instance_dir){}; }; } // namespace multipass diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h index e158da3107..e65df896d6 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h @@ -28,7 +28,8 @@ namespace multipass::hyperv::hcn * Strongly-typed string values for * network policy types. * - * @ref https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/51b2c0024ce9fc0c9c240fe8e14b170e05c57099/virtualization/api/hcn/HNS_Schema.md?plain=1#L522 + * @ref + * https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/51b2c0024ce9fc0c9c240fe8e14b170e05c57099/virtualization/api/hcn/HNS_Schema.md?plain=1#L522 */ struct HcnNetworkPolicyType { diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 664c8c6c57..41f5ab1c84 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -56,7 +56,7 @@ struct CreateComputeSystemParameters /** * Path to the Primary (boot) VHDX file */ - std::filesystem::path vhdx_path{}; + std::filesystem::path vhdx_path{}; /** * List of endpoints that'll be added to the compute system diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 647a76e8ed..ff1a950c03 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -89,7 +89,9 @@ auto resolve_ip_addresses(const std::string& hostname) // Wrap the raw addrinfo pointer so it's always destroyed properly. const auto& [result, addr_info] = [&]() { struct addrinfo* result = {nullptr}; - struct addrinfo hints{}; + struct addrinfo hints + { + }; const auto r = getaddrinfo(hostname.c_str(), nullptr, nullptr, &result); return std::make_pair(r, std::unique_ptr{result, freeaddrinfo}); }(); diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 9ce3272bb0..6a60576843 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -16,6 +16,7 @@ */ #include +#include #include #include #include @@ -26,7 +27,6 @@ #include #include #include -#include #include "backends/hyperv/hyperv_virtual_machine_factory.h" #include "backends/hyperv_api/hcs_virtual_machine_factory.h" @@ -635,7 +635,8 @@ mp::platform::wsa_init_wrapper::wsa_init_wrapper() constexpr auto category = "wsa-init-wrapper"; mpl::debug(category, " initialized WSA, status `{}`", wsa_init_result); - if(!operator bool()){ + if (!operator bool()) + { mpl::error(category, " WSAStartup failed with `{}`: {}", std::system_category().message(wsa_init_result)); } } @@ -800,7 +801,8 @@ std::map mp::platform::Platform::get_netw }; ULONG needed_size{0}; - constexpr auto flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_ALL_INTERFACES; + constexpr auto flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | + GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_ALL_INTERFACES; // Learn how much space we need to allocate. GetAdaptersAddresses(AF_UNSPEC, flags, NULL, nullptr, &needed_size); @@ -808,7 +810,8 @@ std::map mp::platform::Platform::get_netw auto adapter_info = reinterpret_cast(adapters_info_raw_storage.get()); - if (const auto result = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapter_info, &needed_size); result == NO_ERROR) + if (const auto result = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapter_info, &needed_size); + result == NO_ERROR) { // Retrieval was successful. The API returns a linked list, so walk over it. for (auto pitr = adapter_info; pitr; pitr = pitr->Next) @@ -817,12 +820,14 @@ std::map mp::platform::Platform::get_netw MIB_IF_ROW2 ifRow{}; ifRow.InterfaceLuid = adapter.Luid; - if (GetIfEntry2(&ifRow) != NO_ERROR) { + if (GetIfEntry2(&ifRow) != NO_ERROR) + { continue; } // Only list the physical interfaces. - if(!ifRow.InterfaceAndOperStatusFlags.HardwareInterface){ + if (!ifRow.InterfaceAndOperStatusFlags.HardwareInterface) + { continue; } @@ -845,7 +850,8 @@ std::map mp::platform::Platform::get_netw const auto& adapter = *pitr; std::wstring name{adapter.FriendlyName}; - if(name == search){ + if (name == search) + { netinfo.links = unicast_addr_to_network(adapter.FirstUnicastAddress); break; } @@ -856,7 +862,8 @@ std::map mp::platform::Platform::get_netw else { // TODO: FormatMessage. - throw GetNetworkInterfacesInfoException{"Failed to retrieve network interface information. Error code: {}", result}; + throw GetNetworkInterfacesInfoException{"Failed to retrieve network interface information. Error code: {}", + result}; } return ret; } From 087bdf1e81989a362b29a24e404a604ccb1d2999 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Mon, 21 Apr 2025 16:16:33 +0300 Subject: [PATCH 34/76] [hyperv-virtdisk] snapshot feature boilerplate --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs_virtual_machine.cpp | 16 +++ .../backends/hyperv_api/hcs_virtual_machine.h | 7 + .../hcs_virtual_machine_factory.cpp | 2 +- .../virtdisk/virtdisk_api_wrapper.cpp | 134 ++++++++++++------ .../virtdisk_create_virtual_disk_params.h | 13 +- .../hyperv_api/virtdisk/virtdisk_snapshot.cpp | 59 ++++++++ .../hyperv_api/virtdisk/virtdisk_snapshot.h | 46 ++++++ tests/hyperv_api/test_ut_hyperv_virtdisk.cpp | 6 +- 9 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp create mode 100644 src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 0031a0ec5a..e29a7cd94f 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -49,6 +49,7 @@ if(WIN32) hcn/hyperv_hcn_network_policy.cpp hcs/hyperv_hcs_api_wrapper.cpp virtdisk/virtdisk_api_wrapper.cpp + virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp hcs_virtual_machine.cpp hcs_virtual_machine_factory.cpp diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index ff1a950c03..4c9d97b8c7 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -515,4 +515,20 @@ std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const // return std::make_unique(this, &key_provider, mount, target, hcs); } +std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const std::string& snapshot_name, + const std::string& comment, + const std::string& instance_id, + const VMSpecs& specs, + std::shared_ptr parent) +{ + throw NotImplementedOnThisBackendException{"Not implemented yet"}; + // Step 1: Check if VM has been shut down + // Step 2: Check if VM already has +} + +std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const QString& filename) +{ + throw NotImplementedOnThisBackendException{"Not implemented yet"}; +} + } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h index c3e0204fee..c10d081f19 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -82,6 +82,13 @@ struct HCSVirtualMachine final : public BaseVirtualMachine { } + std::shared_ptr make_specific_snapshot(const QString& filename) override; + std::shared_ptr make_specific_snapshot(const std::string& snapshot_name, + const std::string& comment, + const std::string& instance_id, + const VMSpecs& specs, + std::shared_ptr parent) override; + private: const VirtualMachineDescription description{}; const std::string primary_network_guid{}; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 0b96bbf51e..82c495ead4 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -265,7 +265,7 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::clone_vm_impl(const std::string& // Copy the VHDX file. virtdisk::CreateVirtualDiskParameters clone_vhdx_params{}; - clone_vhdx_params.source = src_vm_vhdx.value(); + clone_vhdx_params.predecessor = virtdisk::SourcePathParameters{src_vm_vhdx.value()}; clone_vhdx_params.path = desc.image.image_path.toStdString(); clone_vhdx_params.size_in_bytes = 0; // use source disk size diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index 678e59fdf0..1141fcb26b 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -31,6 +31,15 @@ namespace multipass::hyperv::virtdisk namespace { +// helper type for the visitor #4 +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; +// explicit deduction guide (not needed as of C++20) +template +overloaded(Ts...) -> overloaded; auto normalize_path(std::filesystem::path p) { @@ -117,56 +126,89 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara parameters.Version = CREATE_VIRTUAL_DISK_VERSION_2; parameters.Version2 = {}; parameters.Version2.MaximumSize = params.size_in_bytes; - - // Tell virtdisk to copy the data from source when it's specified. - std::wstring source_path_normalized{}; - if (params.source.has_value()) - { - source_path_normalized = normalize_path(params.source.value()).wstring(); - parameters.Version2.SourcePath = source_path_normalized.c_str(); - - VirtualDiskInfo src_disk_info{}; - const auto result = get_virtual_disk_info(source_path_normalized, src_disk_info); - mpl::debug(kLogCategory, "create_virtual_disk(...) > source disk info fetch result `{}`", result); - - if (src_disk_info.virtual_storage_type) - { - if (src_disk_info.virtual_storage_type == "vhd") - { - parameters.Version2.SourceVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD; - } - else if (src_disk_info.virtual_storage_type == "vhdx") - { - parameters.Version2.SourceVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; - } - else if (src_disk_info.virtual_storage_type == "unknown") + parameters.Version2.SourcePath = nullptr; + parameters.Version2.ParentPath = nullptr; + parameters.Version2.BlockSizeInBytes = CREATE_VIRTUAL_DISK_PARAMETERS_DEFAULT_BLOCK_SIZE; + parameters.Version2.SectorSizeInBytes = CREATE_VIRTUAL_DISK_PARAMETERS_DEFAULT_SECTOR_SIZE; + + CREATE_VIRTUAL_DISK_FLAG flags{CREATE_VIRTUAL_DISK_FLAG_NONE}; + + /** + * The source/parent paths need to be normalized first, + * and the normalized path needs to outlive the API call itself. + */ + std::wstring predecessor_path_normalized{}; + + auto fill_target = + [this](const std::wstring& predecessor_path, PCWSTR& target_path, VIRTUAL_STORAGE_TYPE& target_type) { + target_path = predecessor_path.c_str(); + VirtualDiskInfo predecessor_disk_info{}; + const auto result = get_virtual_disk_info(predecessor_path, predecessor_disk_info); + mpl::debug(kLogCategory, "create_virtual_disk(...) > source disk info fetch result `{}`", result); + if (predecessor_disk_info.virtual_storage_type) { - throw std::runtime_error{"Unable to determine the source disk type."}; + if (predecessor_disk_info.virtual_storage_type == "vhd") + { + target_type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD; + } + else if (predecessor_disk_info.virtual_storage_type == "vhdx") + { + target_type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; + } + else if (predecessor_disk_info.virtual_storage_type == "unknown") + { + throw std::runtime_error{"Unable to determine the source disk type."}; + } + else + { + throw std::runtime_error{"Unsupported source disk type for clone/snapshot operation"}; + } } else { - throw std::runtime_error{"Unsupported source disk type for clone operation"}; + throw std::runtime_error{"Failed to retrieve source disk type for clone/snapshot operation"}; } - } - - mpl::debug(kLogCategory, - "create_virtual_disk(...) > cloning `{}` to `{}`", - std::filesystem::path{source_path_normalized}, - std::filesystem::path{target_path_normalized}); - } - - // - // Internal size of the virtual disk object blocks, in bytes. - // For VHDX this must be a multiple of 1 MB between 1 and 256 MB. - // For VHD 1 this must be set to one of the following values. - // parameters.Version2.BlockSizeInBytes - // - parameters.Version2.BlockSizeInBytes = 1048576; // 1024 KiB - - if (params.path.extension() == ".vhd") - { - parameters.Version2.BlockSizeInBytes = 524288; // 512 KiB - } + }; + + std::visit(overloaded{ + [&](const std::monostate&) { + // + // If there's no source or parent: + // + // Internal size of the virtual disk object blocks, in bytes. + // For VHDX this must be a multiple of 1 MB between 1 and 256 MB. + // For VHD 1 this must be set to one of the following values. + // parameters.Version2.BlockSizeInBytes + // + parameters.Version2.BlockSizeInBytes = 1048576; // 1024 KiB + + if (params.path.extension() == ".vhd") + { + parameters.Version2.BlockSizeInBytes = 524288; // 512 KiB + } + }, + [&](const SourcePathParameters& params) { + predecessor_path_normalized = normalize_path(params.path).wstring(); + fill_target(predecessor_path_normalized, + parameters.Version2.SourcePath, + parameters.Version2.SourceVirtualStorageType); + flags |= CREATE_VIRTUAL_DISK_FLAG_PREVENT_WRITES_TO_SOURCE_DISK; + mpl::debug(kLogCategory, + "create_virtual_disk(...) > cloning `{}` to `{}`", + std::filesystem::path{predecessor_path_normalized}, + std::filesystem::path{target_path_normalized}); + }, + [&](const ParentPathParameters& params) { + predecessor_path_normalized = normalize_path(params.path).wstring(); + fill_target(predecessor_path_normalized, + parameters.Version2.ParentPath, + parameters.Version2.ParentVirtualStorageType); + flags |= CREATE_VIRTUAL_DISK_FLAG_PREVENT_WRITES_TO_SOURCE_DISK; + // Use parent's size. + parameters.Version2.MaximumSize = 0; + }, + }, + params.predecessor); HANDLE result_handle{nullptr}; @@ -178,7 +220,7 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara // [in, optional] PSECURITY_DESCRIPTOR SecurityDescriptor, nullptr, // [in] CREATE_VIRTUAL_DISK_FLAG Flags, - CREATE_VIRTUAL_DISK_FLAG_NONE, + flags, // [in] ULONG ProviderSpecificFlags, 0, // [in] PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h index 33dc1c4626..5155aae2ed 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h @@ -19,13 +19,22 @@ #define MULTIPASS_HYPERV_API_VIRTDISK_CREATE_VIRTUAL_DISK_PARAMETERS_H #include -#include +#include #include namespace multipass::hyperv::virtdisk { +struct SourcePathParameters +{ + std::filesystem::path path; +}; +struct ParentPathParameters +{ + std::filesystem::path path; +}; + /** * Parameters for creating a new virtual disk drive. */ @@ -33,7 +42,7 @@ struct CreateVirtualDiskParameters { std::uint64_t size_in_bytes{}; std::filesystem::path path{}; - std::optional source{}; + std::variant predecessor{}; }; } // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp new file mode 100644 index 0000000000..67f43ce619 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include + +namespace multipass::hyperv::virtdisk +{ + +// on delete: merge to parent + +VirtDiskSnapshot::VirtDiskSnapshot(const std::string& name, + const std::string& comment, + const std::string& cloud_init_instance_id, + std::shared_ptr parent, + const VMSpecs& specs, + const VirtualMachine& vm) + : BaseSnapshot(name, comment, cloud_init_instance_id, std::move(parent), specs, vm) +{ +} + +VirtDiskSnapshot::VirtDiskSnapshot(const QString& filename, VirtualMachine& vm, const VirtualMachineDescription& desc) + : BaseSnapshot(filename, vm, desc) +{ + // std::filesystem::path target_file_path{filename.toStdString()}; + // std::filesystem::path vm_disk_path{desc.image.image_path.toStdString()}; +} + +// Create a differencing disk. + +void VirtDiskSnapshot::capture_impl() +{ +} + +void VirtDiskSnapshot::erase_impl() +{ +} + +void VirtDiskSnapshot::apply_impl() +{ +} + +} // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h new file mode 100644 index 0000000000..2c0d992cc3 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h @@ -0,0 +1,46 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_VIRTDISK_SNAPSHOT_H +#define MULTIPASS_HYPERV_API_VIRTDISK_SNAPSHOT_H + +#include + +#include + +namespace multipass::hyperv::virtdisk +{ + +class VirtDiskSnapshot : public BaseSnapshot +{ + VirtDiskSnapshot(const std::string& name, + const std::string& comment, + const std::string& cloud_init_instance_id, + std::shared_ptr parent, + const VMSpecs& specs, + const VirtualMachine& vm); + VirtDiskSnapshot(const QString& filename, VirtualMachine& vm, const VirtualMachineDescription& desc); + +protected: + void capture_impl() override; + void erase_impl() override; + void apply_impl() override; +}; + +} // namespace multipass::hyperv::virtdisk + +#endif diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index 7c1ec13199..a3ea301037 100644 --- a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -268,12 +268,12 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) ASSERT_EQ(VirtualDiskAccessMask, VIRTUAL_DISK_ACCESS_NONE); ASSERT_EQ(nullptr, SecurityDescriptor); - ASSERT_EQ(CREATE_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_EQ(CREATE_VIRTUAL_DISK_FLAG_PREVENT_WRITES_TO_SOURCE_DISK, Flags); ASSERT_EQ(0, ProviderSpecificFlags); ASSERT_NE(nullptr, Parameters); ASSERT_EQ(Parameters->Version, CREATE_VIRTUAL_DISK_VERSION_2); ASSERT_EQ(Parameters->Version2.MaximumSize, 0); - ASSERT_EQ(Parameters->Version2.BlockSizeInBytes, 1048576); + ASSERT_EQ(Parameters->Version2.BlockSizeInBytes, CREATE_VIRTUAL_DISK_PARAMETERS_DEFAULT_BLOCK_SIZE); ASSERT_STREQ(Parameters->Version2.SourcePath, L"source.vhdx"); ASSERT_EQ(Parameters->Version2.SourceVirtualStorageType.DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); ASSERT_EQ(Parameters->Version2.SourceVirtualStorageType.VendorId, @@ -347,7 +347,7 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) } hyperv::virtdisk::CreateVirtualDiskParameters params{}; - params.source = "source.vhdx"; + params.predecessor = hyperv::virtdisk::SourcePathParameters{"source.vhdx"}; params.path = "test.vhdx"; params.size_in_bytes = 0; From db12e83858a37919e2db87512fcd4b1ab66e9ce7 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 15:27:19 +0300 Subject: [PATCH 35/76] [base_virtual_machine] add predicate to view_snapshots function view_snapshots function now takes a predicate as argument. By default, it's an empty one. The predicate is only evaluated when user provides a predicate. The returned subset of snapshots will be those the predicate is "true" for. This allows user to filter snapshots based on a criterion. --- include/multipass/virtual_machine.h | 4 +++- .../backends/shared/base_virtual_machine.cpp | 12 ++++++++---- src/platform/backends/shared/base_virtual_machine.h | 2 +- tests/mock_virtual_machine.h | 2 +- tests/stub_virtual_machine.h | 2 +- tests/test_base_virtual_machine.cpp | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 5a643c719a..28a4eccb65 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -33,6 +33,7 @@ #include #include #include +#include #include @@ -104,7 +105,8 @@ class VirtualMachine : private DisabledCopyMove const VMMount& mount) = 0; using SnapshotVista = std::vector>; // using vista to avoid confusion with C++ views - virtual SnapshotVista view_snapshots() const = 0; + using SnapshotPredicate = std::function; + virtual SnapshotVista view_snapshots(SnapshotPredicate predicate = {}) const = 0; virtual int get_num_snapshots() const = 0; virtual std::shared_ptr get_snapshot(const std::string& name) const = 0; diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index db5c3f3832..6d326f864e 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -340,17 +340,21 @@ std::vector mp::BaseVirtualMachine::get_all_ipv4() return all_ipv4; } -auto mp::BaseVirtualMachine::view_snapshots() const -> SnapshotVista +auto mp::BaseVirtualMachine::view_snapshots(SnapshotPredicate predicate) const -> SnapshotVista { require_snapshots_support(); SnapshotVista ret; const std::unique_lock lock{snapshot_mutex}; ret.reserve(snapshots.size()); - std::transform(std::cbegin(snapshots), std::cend(snapshots), std::back_inserter(ret), [](const auto& pair) { - return pair.second; - }); + for (const auto& [key, snapshot] : snapshots) + { + if (!predicate || predicate(*snapshot)) + { + ret.push_back(snapshot); + } + } return ret; } diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 2ee0f9b709..7ae08a8a03 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -65,7 +65,7 @@ class BaseVirtualMachine : public VirtualMachine throw NotImplementedOnThisBackendException("native mounts"); } - SnapshotVista view_snapshots() const override; + SnapshotVista view_snapshots(SnapshotPredicate predicate = {}) const override; int get_num_snapshots() const override; std::shared_ptr get_snapshot(const std::string& name) const override; diff --git a/tests/mock_virtual_machine.h b/tests/mock_virtual_machine.h index c9e5ccad64..2178b11e8c 100644 --- a/tests/mock_virtual_machine.h +++ b/tests/mock_virtual_machine.h @@ -85,7 +85,7 @@ struct MockVirtualMachineT : public T make_native_mount_handler, (const std::string&, const VMMount&), (override)); - MOCK_METHOD(VirtualMachine::SnapshotVista, view_snapshots, (), (const, override)); + MOCK_METHOD(VirtualMachine::SnapshotVista, view_snapshots, (VirtualMachine::SnapshotPredicate), (const, override)); MOCK_METHOD(int, get_num_snapshots, (), (const, override)); MOCK_METHOD(std::shared_ptr, get_snapshot, (const std::string&), (const, override)); MOCK_METHOD(std::shared_ptr, get_snapshot, (int index), (const, override)); diff --git a/tests/stub_virtual_machine.h b/tests/stub_virtual_machine.h index f74d716eab..024fafc870 100644 --- a/tests/stub_virtual_machine.h +++ b/tests/stub_virtual_machine.h @@ -133,7 +133,7 @@ struct StubVirtualMachine final : public multipass::VirtualMachine return std::make_unique(); } - SnapshotVista view_snapshots() const override + SnapshotVista view_snapshots(SnapshotPredicate) const override { return {}; } diff --git a/tests/test_base_virtual_machine.cpp b/tests/test_base_virtual_machine.cpp index 276c1d93e3..6ab9c244af 100644 --- a/tests/test_base_virtual_machine.cpp +++ b/tests/test_base_virtual_machine.cpp @@ -477,7 +477,7 @@ TEST_F(BaseVM, providesSnapshotsView) vm.delete_snapshot(sname(i)); ASSERT_EQ(vm.get_num_snapshots(), 4); - auto snapshots = vm.view_snapshots(); + auto snapshots = vm.view_snapshots({}); EXPECT_THAT(snapshots, SizeIs(4)); From abf7f916b896f84d8a310f5cead1273e6fa96a2d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 18:35:15 +0300 Subject: [PATCH 36/76] [hyperv-hcs-vm] implement grant_access_to_paths & get_primary_disk_path --- .../hyperv_api/hcs_virtual_machine.cpp | 90 ++++++++++++------- .../backends/hyperv_api/hcs_virtual_machine.h | 7 ++ 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 4c9d97b8c7..97ac6fdf52 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -44,6 +44,7 @@ namespace * Category for the log messages. */ constexpr auto kLogCategory = "HyperV-Virtual-Machine"; +constexpr auto kVhdxFileName = "current.vhdx"; constexpr auto kDefaultSSHPort = 22; namespace mpl = multipass::logging; @@ -199,23 +200,56 @@ HCSVirtualMachine::HCSVirtualMachine(hcs_sptr_t hcs_w, update_state(); } -bool HCSVirtualMachine::maybe_create_compute_system() +std::filesystem::path HCSVirtualMachine::get_primary_disk_path() const { - hcs::ComputeSystemState cs_state{hcs::ComputeSystemState::unknown}; + const std::filesystem::path base_vhdx = description.image.image_path.toStdString(); + const std::filesystem::path head_avhdx = base_vhdx.parent_path() / virtdisk::VirtDiskSnapshot::head_disk_name(); + return std::filesystem::exists(head_avhdx) ? head_avhdx : base_vhdx; +} - // Check if the VM already exist - const auto result = hcs->get_compute_system_state(vm_name, cs_state); +void HCSVirtualMachine::grant_access_to_paths(std::list paths) const +{ + // std::list, because we need iterator and pointer stability while inserting. + // Normal for loop here because we want .end() to be evaluated in every + // iteration since we might also insert new elements to the list. + for (auto itr = paths.begin(); itr != paths.end(); ++itr) + { + const auto& path = *itr; + mpl::debug(kLogCategory, "Granting access to path `{}`, exists? {}", path, std::filesystem::exists(path)); + if (std::filesystem::is_symlink(path)) + { + paths.push_back(std::filesystem::canonical(path)); + } + + if (const auto r = hcs->grant_vm_access(vm_name, path); !r) + { + mpl::error(kLogCategory, + "Could not grant access to VM `{}` for the path `{}`, error code: {}", + vm_name, + path, + r); + } + } +} - if (!(E_INVALIDARG == static_cast(result.code))) +bool HCSVirtualMachine::maybe_create_compute_system() +{ { - // Target compute system already exist. - return false; + hcs::ComputeSystemState cs_state{hcs::ComputeSystemState::unknown}; + // Check if the VM already exist + const auto result = hcs->get_compute_system_state(vm_name, cs_state); + + if (!(E_INVALIDARG == static_cast(result.code))) + { + // Target compute system already exist, no need to re-create. + return false; + } } // FIXME: Handle suspend state? const auto create_endpoint_params = [this]() { - std::vector endpoint_params{}; + std::vector params{}; // The primary endpoint (management) hcn::CreateEndpointParameters primary_endpoint{}; @@ -223,7 +257,7 @@ bool HCSVirtualMachine::maybe_create_compute_system() primary_endpoint.endpoint_guid = mac2uuid(description.default_mac_address); primary_endpoint.mac_address = description.default_mac_address; replace_colon_with_dash(primary_endpoint.mac_address.value()); - endpoint_params.push_back(primary_endpoint); + params.push_back(primary_endpoint); // Additional endpoints, a.k.a. extra interfaces. for (const auto& v : description.extra_interfaces) @@ -233,9 +267,9 @@ bool HCSVirtualMachine::maybe_create_compute_system() extra_endpoint.endpoint_guid = mac2uuid(v.mac_address); extra_endpoint.mac_address = v.mac_address; replace_colon_with_dash(extra_endpoint.mac_address.value()); - endpoint_params.push_back(extra_endpoint); + params.push_back(extra_endpoint); } - return endpoint_params; + return params; }(); for (const auto& endpoint : create_endpoint_params) @@ -257,15 +291,15 @@ bool HCSVirtualMachine::maybe_create_compute_system() // E_INVALIDARG means there's no such VM. // Create the VM from scratch. - const auto ccs_params = [this, &create_endpoint_params]() { - hcs::CreateComputeSystemParameters ccs_params{}; - ccs_params.name = description.vm_name; - ccs_params.memory_size_mb = description.mem_size.in_megabytes(); - ccs_params.processor_count = description.num_cores; - ccs_params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); - ccs_params.vhdx_path = description.image.image_path.toStdString(); - - static auto create_to_add = [this](const auto& create_params) { + const auto create_compute_system_params = [this, &create_endpoint_params]() { + hcs::CreateComputeSystemParameters params{}; + params.name = description.vm_name; + params.memory_size_mb = description.mem_size.in_megabytes(); + params.processor_count = description.num_cores; + params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); + params.vhdx_path = get_primary_disk_path(); + + const auto create_to_add = [this](const auto& create_params) { hcs::AddEndpointParameters add_params{}; add_params.endpoint_guid = create_params.endpoint_guid; if (!create_params.mac_address) @@ -279,27 +313,19 @@ bool HCSVirtualMachine::maybe_create_compute_system() std::transform(create_endpoint_params.begin(), create_endpoint_params.end(), - std::back_inserter(ccs_params.endpoints), + std::back_inserter(params.endpoints), create_to_add); - return ccs_params; + return params; }(); - if (const auto create_result = hcs->create_compute_system(ccs_params); !create_result) + if (const auto create_result = hcs->create_compute_system(create_compute_system_params); !create_result) { fmt::print(L"Create compute system failed: {}", create_result.status_msg); throw CreateComputeSystemException{"create_compute_system failed with {}", create_result.code}; } // Grant access to the VHDX and the cloud-init ISO files. - const auto grant_paths = {ccs_params.cloudinit_iso_path, ccs_params.vhdx_path}; - - for (const auto& path : grant_paths) - { - if (!hcs->grant_vm_access(ccs_params.name, path)) - { - throw GrantVMAccessException{"Could not grant access to VM `{}` for the path `{}`", ccs_params.name, path}; - } - } + grant_access_to_paths({create_compute_system_params.cloudinit_iso_path, create_compute_system_params.vhdx_path}); return true; } diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h index c10d081f19..758ce0bb2a 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -108,6 +108,13 @@ struct HCSVirtualMachine final : public BaseVirtualMachine * @return false The compute system is already present */ bool maybe_create_compute_system() noexcept(false); + + /** + * Retrieve path to the primary disk symbolic link + */ + std::filesystem::path get_primary_disk_path() const noexcept(false); + + void grant_access_to_paths(std::list paths) const; }; } // namespace multipass::hyperv From c800438273c6950767a2913693b6ca73d6e12ab6 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 18:39:47 +0300 Subject: [PATCH 37/76] [hyperv-virtdisk-snapshot] implement the snapshot feature The snapshot feature is implemented based on virtdisk's differencing disks. When the user wants to create a new snapshot, the implementation creates a new differencing disk for the snapshot. The created snapshot disk is read-only. Another disk called "head" is created for the future writes. When the user wants to create another snapshot, the head becomes the new snapshot and a new empty "head" is created. When user wants to restore to a previous snapshot, the head moves from one parent to the another. The contents in head won't be preserved as the block histories between the parents are not compatible. The user has option to preserve the existing head's state by creating a new snapshot. When a snapshot is deleted, its' contents are merged into the parent and all the children disks are reparented to the parent. - open_virtual_disk now takes access_mask, flags and params as parameter - added merge_virtual_disk_to_parent function - added reparent_virtual_disk function --- .../hyperv_api/hcs_virtual_machine.cpp | 14 +- .../hyperv_api/virtdisk/virtdisk_api_table.h | 4 + .../virtdisk/virtdisk_api_wrapper.cpp | 114 ++++++++++++-- .../virtdisk/virtdisk_api_wrapper.h | 30 +++- .../hyperv_api/virtdisk/virtdisk_snapshot.cpp | 145 ++++++++++++++++-- .../hyperv_api/virtdisk/virtdisk_snapshot.h | 27 +++- .../virtdisk/virtdisk_wrapper_interface.h | 3 + tests/hyperv_api/test_ut_hyperv_virtdisk.cpp | 4 + 8 files changed, 315 insertions(+), 26 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 97ac6fdf52..6647d232bd 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -547,14 +548,19 @@ std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const std::s const VMSpecs& specs, std::shared_ptr parent) { - throw NotImplementedOnThisBackendException{"Not implemented yet"}; - // Step 1: Check if VM has been shut down - // Step 2: Check if VM already has + return std::make_shared(snapshot_name, + comment, + instance_id, + parent, + specs, + *this, + description, + virtdisk); } std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const QString& filename) { - throw NotImplementedOnThisBackendException{"Not implemented yet"}; + return std::make_shared(filename, *this, description, virtdisk); } } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_table.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_table.h index e77fe0d8cf..4eed813cc2 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_table.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_table.h @@ -43,8 +43,12 @@ struct VirtDiskAPITable std::function OpenVirtualDisk = &::OpenVirtualDisk; // @ref https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-resizevirtualdisk std::function ResizeVirtualDisk = &::ResizeVirtualDisk; + // @ref https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-mergevirtualdisk + std::function MergeVirtualDisk = &::MergeVirtualDisk; // @ref https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-getvirtualdiskinformation std::function GetVirtualDiskInformation = &::GetVirtualDiskInformation; + // @ref https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-setvirtualdiskinformation + std::function SetVirtualDiskInformation = &::SetVirtualDiskInformation; // @ref https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle std::function CloseHandle = &::CloseHandle; }; diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index 1141fcb26b..176e46bb00 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -24,6 +24,8 @@ // clang-format on #include + +#include #include namespace multipass::hyperv::virtdisk @@ -52,12 +54,21 @@ using UniqueHandle = std::unique_ptr, decltype(Vir namespace mpl = logging; using lvl = mpl::Level; +struct VirtDiskCreateError : FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + /** * Category for the log messages. */ constexpr auto kLogCategory = "HyperV-VirtDisk-Wrapper"; -UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, const std::filesystem::path& vhdx_path) +UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, + const std::filesystem::path& vhdx_path, + VIRTUAL_DISK_ACCESS_MASK access_mask = VIRTUAL_DISK_ACCESS_MASK::VIRTUAL_DISK_ACCESS_ALL, + OPEN_VIRTUAL_DISK_FLAG flags = OPEN_VIRTUAL_DISK_FLAG::OPEN_VIRTUAL_DISK_FLAG_NONE, + POPEN_VIRTUAL_DISK_PARAMETERS params = nullptr) { mpl::debug(kLogCategory, "open_virtual_disk(...) > vhdx_path: {}", vhdx_path.string()); // @@ -77,16 +88,17 @@ UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, const std::filesyste // [in] PCWSTR Path path_w.c_str(), // [in] VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask - VIRTUAL_DISK_ACCESS_ALL, + access_mask, // [in] OPEN_VIRTUAL_DISK_FLAG Flags - OPEN_VIRTUAL_DISK_FLAG_NONE, + flags, // [in, optional] POPEN_VIRTUAL_DISK_PARAMETERS Parameters - nullptr, + params, // [out] PHANDLE Handle &handle); if (!(result == ERROR_SUCCESS)) { + std::error_code ec{static_cast(result), std::system_category()}; mpl::error(kLogCategory, "open_virtual_disk(...) > OpenVirtualDisk failed with: {}", result); return UniqueHandle{nullptr, api.CloseHandle}; } @@ -109,7 +121,7 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara { mpl::debug(kLogCategory, "create_virtual_disk(...) > params: {}", params); - const auto target_path_normalized = normalize_path(params.path).wstring(); + const auto target_path_normalized = normalize_path(params.path).generic_wstring(); // // https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Hyper-V/Storage/cpp/CreateVirtualDisk.cpp // @@ -119,7 +131,7 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara // Specify UNKNOWN for both device and vendor so the system will use the // file extension to determine the correct VHD format. // - type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN; + type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; type.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN; CREATE_VIRTUAL_DISK_PARAMETERS parameters{}; @@ -141,10 +153,16 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara auto fill_target = [this](const std::wstring& predecessor_path, PCWSTR& target_path, VIRTUAL_STORAGE_TYPE& target_type) { + std::filesystem::path pp{predecessor_path}; + if (!std::filesystem::exists(pp)) + { + throw VirtDiskCreateError{"Predecessor VHDX file `{}` does not exist!", pp}; + } target_path = predecessor_path.c_str(); VirtualDiskInfo predecessor_disk_info{}; const auto result = get_virtual_disk_info(predecessor_path, predecessor_disk_info); mpl::debug(kLogCategory, "create_virtual_disk(...) > source disk info fetch result `{}`", result); + target_type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; if (predecessor_disk_info.virtual_storage_type) { if (predecessor_disk_info.virtual_storage_type == "vhd") @@ -157,16 +175,18 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara } else if (predecessor_disk_info.virtual_storage_type == "unknown") { - throw std::runtime_error{"Unable to determine the source disk type."}; + throw VirtDiskCreateError{"Unable to determine predecessor disk's (`{}`) type!", pp}; } else { - throw std::runtime_error{"Unsupported source disk type for clone/snapshot operation"}; + throw VirtDiskCreateError{"Unsupported predecessor disk type"}; } } else { - throw std::runtime_error{"Failed to retrieve source disk type for clone/snapshot operation"}; + throw VirtDiskCreateError{"Failed to retrieve the predecessor disk type for `{}`, error code: {}", + pp, + result}; } }; @@ -204,6 +224,7 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara parameters.Version2.ParentPath, parameters.Version2.ParentVirtualStorageType); flags |= CREATE_VIRTUAL_DISK_FLAG_PREVENT_WRITES_TO_SOURCE_DISK; + parameters.Version2.ParentVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; // Use parent's size. parameters.Version2.MaximumSize = 0; }, @@ -276,6 +297,8 @@ OperationResult VirtDiskWrapper::resize_virtual_disk(const std::filesystem::path return OperationResult{NOERROR, L""}; } + // auto log_and_yield + mpl::error(kLogCategory, "resize_virtual_disk(...) > ResizeVirtualDisk failed with {}!", resize_result); return OperationResult{E_FAIL, fmt::format(L"ResizeVirtualDisk failed with {}!", resize_result)}; @@ -283,6 +306,79 @@ OperationResult VirtDiskWrapper::resize_virtual_disk(const std::filesystem::path // --------------------------------------------------------- +OperationResult VirtDiskWrapper::merge_virtual_disk_to_parent(const std::filesystem::path& child) const +{ + // https://github.com/microsoftarchive/msdn-code-gallery-microsoft/blob/master/OneCodeTeam/Demo%20various%20VHD%20API%20usage%20(CppVhdAPI)/%5BC%2B%2B%5D-Demo%20various%20VHD%20API%20usage%20(CppVhdAPI)/C%2B%2B/CppVhdAPI/CppVhdAPI.cpp + mpl::debug(kLogCategory, "merge_virtual_disk_to_parent(...) > child: {}", child.string()); + + OPEN_VIRTUAL_DISK_PARAMETERS open_params{}; + open_params.Version = OPEN_VIRTUAL_DISK_VERSION_1; + open_params.Version1.RWDepth = 2; + + const auto child_handle = open_virtual_disk(api, + child, + VIRTUAL_DISK_ACCESS_METAOPS | VIRTUAL_DISK_ACCESS_GET_INFO, + OPEN_VIRTUAL_DISK_FLAG_NONE, + &open_params); + + if (nullptr == child_handle) + { + return OperationResult{E_FAIL, L"open_virtual_disk failed!"}; + } + MERGE_VIRTUAL_DISK_PARAMETERS params{}; + params.Version = MERGE_VIRTUAL_DISK_VERSION_1; + params.Version1.MergeDepth = MERGE_VIRTUAL_DISK_DEFAULT_MERGE_DEPTH; + + if (const auto r = api.MergeVirtualDisk(child_handle.get(), MERGE_VIRTUAL_DISK_FLAG_NONE, ¶ms, nullptr); + r == ERROR_SUCCESS) + return OperationResult{NOERROR, L""}; + else + { + std::error_code ec{static_cast(r), std::system_category()}; + mpl::error(kLogCategory, "merge_virtual_disk_to_parent(...) > MergeVirtualDisk failed with {}!", ec.message()); + return OperationResult{E_FAIL, fmt::format(L"MergeVirtualDisk failed with {}!", r)}; + } +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::reparent_virtual_disk(const std::filesystem::path& child, + const std::filesystem::path& parent) const +{ + mpl::debug(kLogCategory, "reparent_virtual_disk(...) > child: {}, new parent: {}", child.string(), parent.string()); + + OPEN_VIRTUAL_DISK_PARAMETERS open_parameters{}; + open_parameters.Version = OPEN_VIRTUAL_DISK_VERSION_2; + open_parameters.Version2.GetInfoOnly = false; + + const auto child_handle = + open_virtual_disk(api, child, VIRTUAL_DISK_ACCESS_NONE, OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS, &open_parameters); + + if (nullptr == child_handle) + { + return OperationResult{E_FAIL, L"open_virtual_disk failed!"}; + } + + const auto parent_path_wstr = parent.generic_wstring(); + + SET_VIRTUAL_DISK_INFO info{}; + // Confusing naming. version field is basically a "request type" field + // for {Get/Set}VirtualDiskInformation. + info.Version = SET_VIRTUAL_DISK_INFO_PARENT_PATH_WITH_DEPTH; + info.ParentPathWithDepthInfo.ParentFilePath = parent_path_wstr.c_str(); + info.ParentPathWithDepthInfo.ChildDepth = 1; // immediate child + + if (const auto r = api.SetVirtualDiskInformation(child_handle.get(), &info); r == ERROR_SUCCESS) + return OperationResult{NOERROR, L""}; + else + { + mpl::error(kLogCategory, "reparent_virtual_disk(...) > SetVirtualDiskInformation failed with {}!", r); + return OperationResult{E_FAIL, fmt::format(L"reparent_virtual_disk failed with {}!", r)}; + } +} + +// --------------------------------------------------------- + OperationResult VirtDiskWrapper::get_virtual_disk_info(const std::filesystem::path& vhdx_path, VirtualDiskInfo& vdinfo) const { diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.h index aedfee658d..2474fa5d51 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.h @@ -69,8 +69,34 @@ struct VirtDiskWrapper : public VirtDiskWrapperInterface * @return An object that evaluates to true on success, false otherwise. * message() may contain details of failure when result is false. */ - virtual OperationResult resize_virtual_disk(const std::filesystem::path& vhdx_path, - std::uint64_t new_size_bytes) const override; + [[nodiscard]] virtual OperationResult resize_virtual_disk(const std::filesystem::path& vhdx_path, + std::uint64_t new_size_bytes) const override; + + // --------------------------------------------------------- + + /** + * Merge a child differencing disk to its parent + * + * @param [in] child Path to the differencing disk + * + * @return An object that evaluates to true on success, false otherwise. + * message() may contain details of failure when result is false. + */ + [[nodiscard]] OperationResult merge_virtual_disk_to_parent(const std::filesystem::path& child) const override; + + // --------------------------------------------------------- + + /** + * Reparent a virtual disk + * + * @param [in] child Path to the virtual disk to reparent + * @param [in] parent Path to the new parent + * + * @return An object that evaluates to true on success, false otherwise. + * message() may contain details of failure when result is false. + */ + [[nodiscard]] virtual OperationResult reparent_virtual_disk(const std::filesystem::path& child, + const std::filesystem::path& parent) const override; // --------------------------------------------------------- diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp index 67f43ce619..12dba17891 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -17,43 +17,170 @@ #include +#include + +#include #include #include +namespace +{ +constexpr auto kLogCategory = "virtdisk-snapshot"; +} + namespace multipass::hyperv::virtdisk { -// on delete: merge to parent +struct CreateVirtdiskSnapshotError : public FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; VirtDiskSnapshot::VirtDiskSnapshot(const std::string& name, const std::string& comment, - const std::string& cloud_init_instance_id, + const std::string& instance_id, std::shared_ptr parent, const VMSpecs& specs, - const VirtualMachine& vm) - : BaseSnapshot(name, comment, cloud_init_instance_id, std::move(parent), specs, vm) + const VirtualMachine& vm, + const VirtualMachineDescription& desc, + virtdisk_sptr_t virtdisk) + : BaseSnapshot(name, comment, instance_id, std::move(parent), specs, vm), + base_vhdx_path{desc.image.image_path.toStdString()}, + vm{vm}, + virtdisk{virtdisk} +{ +} + +VirtDiskSnapshot::VirtDiskSnapshot(const QString& filename, + VirtualMachine& vm, + const VirtualMachineDescription& desc, + virtdisk_sptr_t virtdisk) + : BaseSnapshot(filename, vm, desc), base_vhdx_path{desc.image.image_path.toStdString()}, vm{vm}, virtdisk{virtdisk} { } -VirtDiskSnapshot::VirtDiskSnapshot(const QString& filename, VirtualMachine& vm, const VirtualMachineDescription& desc) - : BaseSnapshot(filename, vm, desc) +std::string VirtDiskSnapshot::make_snapshot_name(const Snapshot& ss) { - // std::filesystem::path target_file_path{filename.toStdString()}; - // std::filesystem::path vm_disk_path{desc.image.image_path.toStdString()}; + constexpr static auto kSnapshotNameFormat = "{}.avhdx"; + return fmt::format(kSnapshotNameFormat, ss.get_name()); } -// Create a differencing disk. +std::filesystem::path VirtDiskSnapshot::make_snapshot_path(const Snapshot& ss) const +{ + return base_vhdx_path.parent_path() / make_snapshot_name(ss); +} void VirtDiskSnapshot::capture_impl() { + assert(virtdisk); + + const auto& head_path = base_vhdx_path.parent_path() / head_disk_name(); + const auto& snapshot_path = make_snapshot_path(*this); + + if (!std::filesystem::exists(head_path)) + { + const auto& parent = get_parent(); + create_new_child_disk(parent ? make_snapshot_path(*parent) : base_vhdx_path, head_path); + } + + // Step 1: Rename current head to snapshot name + std::filesystem::rename(head_path, snapshot_path); + + // Step 2: Create a new head from the snapshot + create_new_child_disk(snapshot_path, head_path); +} + +void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent, + const std::filesystem::path& child) const +{ + if (!std::filesystem::exists(parent)) + { + throw CreateVirtdiskSnapshotError("The parent disk {} does not exist!", child); + } + + if (std::filesystem::exists(child)) + { + throw CreateVirtdiskSnapshotError("The target child disk {} already exist!", child); + } + + virtdisk::CreateVirtualDiskParameters params{}; + params.path = child; + params.predecessor = virtdisk::ParentPathParameters{parent}; + + if (const auto r = virtdisk->create_virtual_disk(params); !r) + throw CreateVirtdiskSnapshotError{ + "Could not create the head differencing disk for the snapshot. Error code: {}", + r}; +} + +void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots, + const std::filesystem::path& new_parent) const +{ + for (const auto& child : snapshots) + { + const auto& child_path = make_snapshot_path(*child); + if (const auto result = virtdisk->reparent_virtual_disk(child_path, new_parent); !result) + { + mpl::warn(kLogCategory, "Could not reparent `{}` to `{}`. Error code: {}", child_path, new_parent, result); + continue; + } + mpl::debug(kLogCategory, "Successfully reparented the child disk `{}` to `{}`", child_path, new_parent); + } } void VirtDiskSnapshot::erase_impl() { + assert(virtdisk); + const auto& parent = get_parent(); + const auto& self_path = make_snapshot_path(*this); + + // 1: Merge this to its parent + if (const auto merge_r = virtdisk->merge_virtual_disk_to_parent(self_path); merge_r) + { + const auto& parent_path = parent ? make_snapshot_path(*parent) : base_vhdx_path; + mpl::debug(kLogCategory, "Successfully merged differencing disk `{}` to its parent", self_path, parent_path); + + // The actual reparenting of the children needs to happen here. + // Reparenting is not a simple "-> now this is your parent" like thing. The children + // include parent's metadata calculated based on actual contents, and merging a child disk + // to parent updates its parent's metadata, too. + // Hence, the reparenting operation not only needs to happen to the orphaned children, + // but also to the existing children of the parent as well, so the updated metadata of the + // parent could be reflected to the all. + const auto children_to_reparent = + vm.view_snapshots([&parent, this_index = this->get_index()](const Snapshot& ss) { + return + // Exclude self. + (ss.get_index() != this_index) && + // set_parent() for the orphaned children happens before erase() call + // so they're already adopted by the self's parent at this point. + (ss.get_parents_index() == parent->get_index()); + }); + reparent_snapshot_disks(children_to_reparent, parent_path); + } + else + { + throw CreateVirtdiskSnapshotError{"Could not merge differencing disk to parent. Error code: {}", merge_r}; + } + // Finally, erase the merged disk. + mpl::debug(kLogCategory, "Removing snapshot file: `{}`", self_path); + std::filesystem::remove(self_path); } void VirtDiskSnapshot::apply_impl() { + assert(virtdisk); + + const auto& head_path = base_vhdx_path.parent_path() / head_disk_name(); + const auto& snapshot_path = make_snapshot_path(*this); + + // Restoring a snapshot means we're discarding the head state. + std::error_code ec{}; + std::filesystem::remove(head_path, ec); + mpl::debug(kLogCategory, "apply_impl() -> {} remove {}", head_path, ec.message()); + + // Create a new head from the snapshot + create_new_child_disk(snapshot_path, head_path); } } // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h index 2c0d992cc3..38124fb942 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h @@ -18,27 +18,50 @@ #ifndef MULTIPASS_HYPERV_API_VIRTDISK_SNAPSHOT_H #define MULTIPASS_HYPERV_API_VIRTDISK_SNAPSHOT_H +#include #include #include +#include namespace multipass::hyperv::virtdisk { class VirtDiskSnapshot : public BaseSnapshot { +public: VirtDiskSnapshot(const std::string& name, const std::string& comment, const std::string& cloud_init_instance_id, std::shared_ptr parent, const VMSpecs& specs, - const VirtualMachine& vm); - VirtDiskSnapshot(const QString& filename, VirtualMachine& vm, const VirtualMachineDescription& desc); + const VirtualMachine& vm, + const VirtualMachineDescription& desc, + virtdisk_sptr_t virtdisk); + VirtDiskSnapshot(const QString& filename, + VirtualMachine& vm, + const VirtualMachineDescription& desc, + virtdisk_sptr_t virtdisk); + + static std::string make_snapshot_name(const Snapshot& ss); + std::filesystem::path make_snapshot_path(const Snapshot& ss) const; + static constexpr std::string_view head_disk_name() noexcept + { + return "head.avhdx"; + } protected: void capture_impl() override; void erase_impl() override; void apply_impl() override; + +private: + void create_new_child_disk(const std::filesystem::path& parent, const std::filesystem::path& child) const; + void reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots, + const std::filesystem::path& new_parent) const; + const std::filesystem::path base_vhdx_path{}; + const VirtualMachine& vm; + virtdisk_sptr_t virtdisk{nullptr}; }; } // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper_interface.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper_interface.h index 566f98a12b..b50be41ae9 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper_interface.h @@ -35,6 +35,9 @@ struct VirtDiskWrapperInterface virtual OperationResult create_virtual_disk(const CreateVirtualDiskParameters& params) const = 0; virtual OperationResult resize_virtual_disk(const std::filesystem::path& vhdx_path, std::uint64_t new_size_bytes) const = 0; + virtual OperationResult merge_virtual_disk_to_parent(const std::filesystem::path& child) const = 0; + virtual OperationResult reparent_virtual_disk(const std::filesystem::path& child, + const std::filesystem::path& parent) const = 0; virtual OperationResult get_virtual_disk_info(const std::filesystem::path& vhdx_path, VirtualDiskInfo& vdinfo) const = 0; virtual ~VirtDiskWrapperInterface() = default; diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index a3ea301037..89d917accc 100644 --- a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -61,7 +61,9 @@ struct HyperVVirtDisk_UnitTests : public ::testing::Test ::testing::MockFunction stub_mock_create_virtual_disk; ::testing::MockFunction stub_mock_open_virtual_disk; ::testing::MockFunction stub_mock_resize_virtual_disk; + ::testing::MockFunction stub_mock_merge_virtual_disk; ::testing::MockFunction stub_mock_get_virtual_disk_information; + ::testing::MockFunction stub_mock_set_virtual_disk_information; ::testing::MockFunction stub_mock_close_handle; // Initialize the API table with stub functions, so if any of these fire without @@ -70,7 +72,9 @@ struct HyperVVirtDisk_UnitTests : public ::testing::Test stub_mock_create_virtual_disk.AsStdFunction(), stub_mock_open_virtual_disk.AsStdFunction(), stub_mock_resize_virtual_disk.AsStdFunction(), + stub_mock_merge_virtual_disk.AsStdFunction(), stub_mock_get_virtual_disk_information.AsStdFunction(), + stub_mock_set_virtual_disk_information.AsStdFunction(), stub_mock_close_handle.AsStdFunction(), }; From b41a29e68a2f7216bc9bbbf49d8b4ac9797cd076 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 18:49:15 +0300 Subject: [PATCH 38/76] [hyperv-hcs-wrapper] grant_vm_access: use normalized path --- src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index b35ddda86d..66f2e4e0c7 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -549,7 +549,7 @@ OperationResult HCSWrapper::grant_vm_access(const std::string& compute_system_na compute_system_name, file_path.string()); - const auto path_as_wstring = file_path.wstring(); + const auto path_as_wstring = file_path.generic_wstring(); const auto csname_as_wstring = string_to_wstring(compute_system_name); const auto result = api.GrantVmAccess(csname_as_wstring.c_str(), path_as_wstring.c_str()); return {result, FAILED(result) ? L"GrantVmAccess failed!" : L""}; From 157f384ff87e710a081cae3f2cb39dcdf26d47f1 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 18:49:47 +0300 Subject: [PATCH 39/76] [hyperv-hcs-vm] start: throw exception when start fails --- src/platform/backends/hyperv_api/hcs_virtual_machine.cpp | 7 +++++++ .../backends/hyperv_api/hcs_virtual_machine_exceptions.h | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 6647d232bd..4eef1cc32b 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -393,6 +393,13 @@ void HCSVirtualMachine::start() } }(); + if (!status) + { + state = VirtualMachine::State::stopped; + update_state(); + throw StartComputeSystemException{"Could not start the VM: {}", status}; + } + mpl::debug(kLogCategory, "start() -> Start/resume VM `{}`, result `{}`", vm_name, status); } void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h index bd470a3b5f..c1d2dd1880 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h @@ -58,6 +58,11 @@ struct ImageResizeException : public FormattedExceptionBase<> using FormattedExceptionBase<>::FormattedExceptionBase; }; +struct StartComputeSystemException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + } // namespace multipass::hyperv #endif From 10491f0170b4a5f0adacb4db9e1341102d999a31 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 21:08:36 +0300 Subject: [PATCH 40/76] [hyperv-api-operation-result] allow cast to std::error_code --- .../backends/hyperv_api/hyperv_api_operation_result.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h index 75343c057a..dd898da280 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h +++ b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h @@ -59,6 +59,11 @@ struct ResultCode return static_cast(result); } + [[nodiscard]] operator std::error_code() const noexcept + { + return std::error_code{result, std::system_category()}; + } + private: HRESULT result{}; }; @@ -88,6 +93,11 @@ struct OperationResult { return static_cast(code); } + + [[nodiscard]] operator std::error_code() const noexcept + { + return code; + } }; } // namespace multipass::hyperv From 3c814a9694cc6a4d56a390021505661d3d655bf8 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 21:19:46 +0300 Subject: [PATCH 41/76] [hyperv-virtdisk-snapshot] tidy-up --- .../virtdisk/virtdisk_api_wrapper.cpp | 11 +-- .../hyperv_api/virtdisk/virtdisk_snapshot.cpp | 67 ++++++++++++----- .../hyperv_api/virtdisk/virtdisk_snapshot.h | 73 ++++++++++++++++++- 3 files changed, 125 insertions(+), 26 deletions(-) diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index 176e46bb00..26f8d7bd80 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -80,9 +80,9 @@ UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, type.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN; HANDLE handle{nullptr}; - const auto path_w = vhdx_path.wstring(); + const auto path_w = vhdx_path.generic_wstring(); - const auto result = api.OpenVirtualDisk( + const ResultCode result = api.OpenVirtualDisk( // [in] PVIRTUAL_STORAGE_TYPE VirtualStorageType &type, // [in] PCWSTR Path @@ -96,10 +96,11 @@ UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, // [out] PHANDLE Handle &handle); - if (!(result == ERROR_SUCCESS)) + if (!result) { - std::error_code ec{static_cast(result), std::system_category()}; - mpl::error(kLogCategory, "open_virtual_disk(...) > OpenVirtualDisk failed with: {}", result); + mpl::error(kLogCategory, + "open_virtual_disk(...) > OpenVirtualDisk failed with: {}", + static_cast(result)); return UniqueHandle{nullptr, api.CloseHandle}; } diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp index 12dba17891..7239dd1798 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -31,9 +31,9 @@ constexpr auto kLogCategory = "virtdisk-snapshot"; namespace multipass::hyperv::virtdisk { -struct CreateVirtdiskSnapshotError : public FormattedExceptionBase<> +struct CreateVirtdiskSnapshotError : public FormattedExceptionBase { - using FormattedExceptionBase<>::FormattedExceptionBase; + using FormattedExceptionBase::FormattedExceptionBase; }; VirtDiskSnapshot::VirtDiskSnapshot(const std::string& name, @@ -59,7 +59,7 @@ VirtDiskSnapshot::VirtDiskSnapshot(const QString& filename, { } -std::string VirtDiskSnapshot::make_snapshot_name(const Snapshot& ss) +std::string VirtDiskSnapshot::make_snapshot_filename(const Snapshot& ss) { constexpr static auto kSnapshotNameFormat = "{}.avhdx"; return fmt::format(kSnapshotNameFormat, ss.get_name()); @@ -67,7 +67,7 @@ std::string VirtDiskSnapshot::make_snapshot_name(const Snapshot& ss) std::filesystem::path VirtDiskSnapshot::make_snapshot_path(const Snapshot& ss) const { - return base_vhdx_path.parent_path() / make_snapshot_name(ss); + return base_vhdx_path.parent_path() / make_snapshot_filename(ss); } void VirtDiskSnapshot::capture_impl() @@ -77,10 +77,13 @@ void VirtDiskSnapshot::capture_impl() const auto& head_path = base_vhdx_path.parent_path() / head_disk_name(); const auto& snapshot_path = make_snapshot_path(*this); + // Check if head disk already exists. The head disk may not exist for a VM + // that has no snapshots yet. if (!std::filesystem::exists(head_path)) { const auto& parent = get_parent(); - create_new_child_disk(parent ? make_snapshot_path(*parent) : base_vhdx_path, head_path); + const auto& target = parent ? make_snapshot_path(*parent) : base_vhdx_path; + create_new_child_disk(target, head_path); } // Step 1: Rename current head to snapshot name @@ -93,35 +96,56 @@ void VirtDiskSnapshot::capture_impl() void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent, const std::filesystem::path& child) const { + assert(virtdisk); + // The parent must already exist. if (!std::filesystem::exists(parent)) - { - throw CreateVirtdiskSnapshotError("The parent disk {} does not exist!", child); - } + throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::no_such_file_or_directory), + "Parent disk `{}` does not exist", + parent}; + // The given child path must not exist if (std::filesystem::exists(child)) - { - throw CreateVirtdiskSnapshotError("The target child disk {} already exist!", child); - } + throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists), + "Child disk `{}` already exists", + child}; virtdisk::CreateVirtualDiskParameters params{}; - params.path = child; params.predecessor = virtdisk::ParentPathParameters{parent}; + params.path = child; + const auto result = virtdisk->create_virtual_disk(params); + if (result) + { + mpl::debug(kLogCategory, "Successfully created the child disk: `{}`", child); + return; + } - if (const auto r = virtdisk->create_virtual_disk(params); !r) - throw CreateVirtdiskSnapshotError{ - "Could not create the head differencing disk for the snapshot. Error code: {}", - r}; + throw CreateVirtdiskSnapshotError{result, "Could not create the head differencing disk for the snapshot"}; } void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots, const std::filesystem::path& new_parent) const { + // The parent must already exist. + if (!std::filesystem::exists(new_parent)) + throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::no_such_file_or_directory), + "Parent disk `{}` does not exist", + new_parent}; + assert(virtdisk); for (const auto& child : snapshots) { const auto& child_path = make_snapshot_path(*child); + + if (std::filesystem::exists(child_path)) + throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists), + "Child disk `{}` already exists", + child_path}; if (const auto result = virtdisk->reparent_virtual_disk(child_path, new_parent); !result) { - mpl::warn(kLogCategory, "Could not reparent `{}` to `{}`. Error code: {}", child_path, new_parent, result); + mpl::warn(kLogCategory, + "Could not reparent `{}` to `{}`: {}", + child_path, + new_parent, + static_cast(result)); continue; } mpl::debug(kLogCategory, "Successfully reparented the child disk `{}` to `{}`", child_path, new_parent); @@ -138,7 +162,10 @@ void VirtDiskSnapshot::erase_impl() if (const auto merge_r = virtdisk->merge_virtual_disk_to_parent(self_path); merge_r) { const auto& parent_path = parent ? make_snapshot_path(*parent) : base_vhdx_path; - mpl::debug(kLogCategory, "Successfully merged differencing disk `{}` to its parent", self_path, parent_path); + mpl::debug(kLogCategory, + "Successfully merged differencing disk `{}` to parent disk `{}`", + self_path, + parent_path); // The actual reparenting of the children needs to happen here. // Reparenting is not a simple "-> now this is your parent" like thing. The children @@ -160,7 +187,7 @@ void VirtDiskSnapshot::erase_impl() } else { - throw CreateVirtdiskSnapshotError{"Could not merge differencing disk to parent. Error code: {}", merge_r}; + throw CreateVirtdiskSnapshotError{merge_r, "Could not merge differencing disk to parent"}; } // Finally, erase the merged disk. mpl::debug(kLogCategory, "Removing snapshot file: `{}`", self_path); @@ -177,7 +204,7 @@ void VirtDiskSnapshot::apply_impl() // Restoring a snapshot means we're discarding the head state. std::error_code ec{}; std::filesystem::remove(head_path, ec); - mpl::debug(kLogCategory, "apply_impl() -> {} remove {}", head_path, ec.message()); + mpl::debug(kLogCategory, "apply_impl() -> {} remove: {}", head_path, ec); // Create a new head from the snapshot create_new_child_disk(snapshot_path, head_path); diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h index 38124fb942..82a35f57e5 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h @@ -27,9 +27,27 @@ namespace multipass::hyperv::virtdisk { +/** + * Virtdisk-based snapshot implementation. + * + * The implementation uses differencing disks to realize the + * functionality. + */ class VirtDiskSnapshot : public BaseSnapshot { public: + /** + * Create a new Virtdisk snapshot + * + * @param [in] name Name for the snapshot + * @param [in] comment Comment (optional) + * @param [in] cloud_init_instance_id Name of the VM + * @param [in] parent Parent, if exists + * @param [in] specs VM specs + * @param [in] vm Snapshot owner VM + * @param [in] desc Owner VM description + * @param [in] virtdisk Virtdisk API object + */ VirtDiskSnapshot(const std::string& name, const std::string& comment, const std::string& cloud_init_instance_id, @@ -38,13 +56,41 @@ class VirtDiskSnapshot : public BaseSnapshot const VirtualMachine& vm, const VirtualMachineDescription& desc, virtdisk_sptr_t virtdisk); + + /** + * Load an existing VirtDiskSnapshot from file + * + * @param [in] filename JSON file + * @param [in] vm Snapshot owner VM + * @param [in] desc Owner VM description + * @param [in] virtdisk Virtdisk API object + */ VirtDiskSnapshot(const QString& filename, VirtualMachine& vm, const VirtualMachineDescription& desc, virtdisk_sptr_t virtdisk); - static std::string make_snapshot_name(const Snapshot& ss); + /** + * Create a consistent filename for a snapshot + * + * @param [in] ss The snapshot + * @return std::string Filename for the snapshot + */ + static std::string make_snapshot_filename(const Snapshot& ss); + + /** + * Retrieve the path for a snapshot + * + * @param [in] ss The snapshot + * @return std::filesystem::path The path that the snapshot is at + */ std::filesystem::path make_snapshot_path(const Snapshot& ss) const; + + /** + * The name for the head disk + * + * @return std::string_view Head disk filename + */ static constexpr std::string_view head_disk_name() noexcept { return "head.avhdx"; @@ -56,11 +102,36 @@ class VirtDiskSnapshot : public BaseSnapshot void apply_impl() override; private: + /** + * Create a new differencing child disk from the parent + * + * @param [in] parent Parent of the new differencing child disk. Must already exist. + * @param [in] child Where to create the child disk. Must be non-existent. + */ void create_new_child_disk(const std::filesystem::path& parent, const std::filesystem::path& child) const; + + /** + * Change the parent disk of the snapshot differencing disks + * + * @param [in] snapshots The list of snapshots to reparent the differencing disks of + * @param [in] new_parent The path to the new parent virtual disk + */ void reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots, const std::filesystem::path& new_parent) const; + + /** + * Path to the base disk, i.e. the ancestor of all differencing disks. + */ const std::filesystem::path base_vhdx_path{}; + + /** + * The owning VM + */ const VirtualMachine& vm; + + /** + * VirtDisk API object + */ virtdisk_sptr_t virtdisk{nullptr}; }; From 493bfc872df713ef792611cd602308ae7a848f8c Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 21:21:42 +0300 Subject: [PATCH 42/76] [file_ops] add std::filesystem::{rename,exists} also add a couple non-std::error_code overloads. mark some of the std::error_code overloads noexcept. updated the test code. --- include/multipass/file_ops.h | 8 ++++++-- src/utils/file_ops.cpp | 23 +++++++++++++++++++++-- tests/mock_file_ops.h | 16 ++++++++++++++-- tests/test_base_snapshot.cpp | 13 +++++++++---- tests/test_image_vault_utils.cpp | 4 ++-- tests/test_sftpserver.cpp | 4 ++-- 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/include/multipass/file_ops.h b/include/multipass/file_ops.h index 24cc912bb1..fdb96a672b 100644 --- a/include/multipass/file_ops.h +++ b/include/multipass/file_ops.h @@ -105,11 +105,15 @@ class FileOps : public Singleton virtual std::unique_ptr open_read(const fs::path& path, std::ios_base::openmode mode = std::ios_base::in) const; virtual void copy(const fs::path& src, const fs::path& dist, fs::copy_options copy_options) const; - virtual bool exists(const fs::path& path, std::error_code& err) const; + virtual void rename(const fs::path& old_p, const fs::path& new_p) const; + virtual void rename(const fs::path& old_p, const fs::path& new_p, std::error_code& ec) const noexcept; + virtual bool exists(const fs::path& path) const; + virtual bool exists(const fs::path& path, std::error_code& err) const noexcept; virtual bool is_directory(const fs::path& path, std::error_code& err) const; virtual bool create_directory(const fs::path& path, std::error_code& err) const; virtual bool create_directories(const fs::path& path, std::error_code& err) const; - virtual bool remove(const fs::path& path, std::error_code& err) const; + virtual bool remove(const fs::path& path) const; + virtual bool remove(const fs::path& path, std::error_code& err) const noexcept; virtual void create_symlink(const fs::path& to, const fs::path& path, std::error_code& err) const; virtual fs::path read_symlink(const fs::path& path, std::error_code& err) const; virtual fs::file_status status(const fs::path& path, std::error_code& err) const; diff --git a/src/utils/file_ops.cpp b/src/utils/file_ops.cpp index 4a3cfc9132..9600838448 100644 --- a/src/utils/file_ops.cpp +++ b/src/utils/file_ops.cpp @@ -222,7 +222,21 @@ void mp::FileOps::copy(const fs::path& src, const fs::path& dist, fs::copy_optio fs::copy(src, dist, copy_options); } -bool mp::FileOps::exists(const fs::path& path, std::error_code& err) const +void mp::FileOps::rename(const fs::path& old_p, const fs::path& new_p) const +{ + fs::rename(old_p, new_p); +} +void mp::FileOps::rename(const fs::path& old_p, const fs::path& new_p, std::error_code& ec) const noexcept +{ + fs::rename(old_p, new_p, ec); +} + +bool mp::FileOps::exists(const fs::path& path) const +{ + return fs::exists(path); +} + +bool mp::FileOps::exists(const fs::path& path, std::error_code& err) const noexcept { return fs::exists(path, err); } @@ -242,7 +256,12 @@ bool mp::FileOps::create_directories(const fs::path& path, std::error_code& err) return fs::create_directories(path, err); } -bool mp::FileOps::remove(const fs::path& path, std::error_code& err) const +bool mp::FileOps::remove(const fs::path& path) const +{ + return fs::remove(path); +} + +bool mp::FileOps::remove(const fs::path& path, std::error_code& err) const noexcept { return fs::remove(path, err); } diff --git a/tests/mock_file_ops.h b/tests/mock_file_ops.h index f945074e68..1fa020347f 100644 --- a/tests/mock_file_ops.h +++ b/tests/mock_file_ops.h @@ -79,11 +79,23 @@ class MockFileOps : public FileOps (override, const)); MOCK_METHOD(std::unique_ptr, open_read, (const fs::path& path, std::ios_base::openmode mode), (override, const)); - MOCK_METHOD(bool, exists, (const fs::path& path, std::error_code& err), (override, const)); + + MOCK_METHOD(void, + copy, + (const fs::path& src, const fs::path& dist, fs::copy_options copy_options), + (override, const)); + MOCK_METHOD(void, rename, (const fs::path& old_p, const fs::path& new_p), (override, const)); + MOCK_METHOD(void, + rename, + (const fs::path& old_p, const fs::path& new_p, std::error_code& errc), + (override, const, noexcept)); + MOCK_METHOD(bool, exists, (const fs::path& path), (override, const)); + MOCK_METHOD(bool, exists, (const fs::path& path, std::error_code& err), (override, const, noexcept)); MOCK_METHOD(bool, is_directory, (const fs::path& path, std::error_code& err), (override, const)); MOCK_METHOD(bool, create_directory, (const fs::path& path, std::error_code& err), (override, const)); MOCK_METHOD(bool, create_directories, (const fs::path& path, std::error_code& err), (override, const)); - MOCK_METHOD(bool, remove, (const fs::path& path, std::error_code& err), (override, const)); + MOCK_METHOD(bool, remove, (const fs::path& path), (override, const)); + MOCK_METHOD(bool, remove, (const fs::path& path, std::error_code& err), (override, const, noexcept)); MOCK_METHOD(void, create_symlink, (const fs::path& to, const fs::path& path, std::error_code& err), (override, const)); MOCK_METHOD(fs::path, read_symlink, (const fs::path& path, std::error_code& err), (override, const)); diff --git a/tests/test_base_snapshot.cpp b/tests/test_base_snapshot.cpp index c6bead9d1c..23b02b0e71 100644 --- a/tests/test_base_snapshot.cpp +++ b/tests/test_base_snapshot.cpp @@ -646,7 +646,8 @@ TEST_F(TestBaseSnapshot, eraseRemovesFile) snapshot.capture(); auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, rename(Property(&QFile::fileName, Eq(expected_file_path)), Ne(expected_file_path))) + EXPECT_CALL(*mock_file_ops, + rename(MatcherCast(Property(&QFile::fileName, Eq(expected_file_path))), Ne(expected_file_path))) .WillOnce(Return(true)); snapshot.erase(); @@ -659,7 +660,8 @@ TEST_F(TestBaseSnapshot, eraseThrowsIfUnableToRenameFile) auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); const auto expected_file_path = derive_persisted_snapshot_file_path(snapshot.get_index()); - EXPECT_CALL(*mock_file_ops, rename(Property(&QFile::fileName, Eq(expected_file_path)), _)).WillOnce(Return(false)); + EXPECT_CALL(*mock_file_ops, rename(MatcherCast(Property(&QFile::fileName, Eq(expected_file_path))), _)) + .WillOnce(Return(false)); EXPECT_CALL(*mock_file_ops, exists(Matcher(Property(&QFile::fileName, Eq(expected_file_path))))) .WillOnce(Return(true)); @@ -682,9 +684,12 @@ TEST_F(TestBaseSnapshot, restoresFileOnFailureToErase) snapshot.capture(); auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, rename(Property(&QFile::fileName, Eq(expected_file_path)), Ne(expected_file_path))) + EXPECT_CALL(*mock_file_ops, + rename(MatcherCast(Property(&QFile::fileName, Eq(expected_file_path))), Ne(expected_file_path))) .WillOnce(Return(true)); - EXPECT_CALL(*mock_file_ops, rename(Property(&QFile::fileName, Ne(expected_file_path)), Eq(expected_file_path))); + EXPECT_CALL( + *mock_file_ops, + rename(MatcherCast(Property(&QFile::fileName, Ne(expected_file_path))), Eq(expected_file_path))); EXPECT_CALL(snapshot, erase_impl).WillOnce([]() { throw std::runtime_error{"test"}; }); diff --git a/tests/test_image_vault_utils.cpp b/tests/test_image_vault_utils.cpp index ef0b27bab1..03ec8dfafd 100644 --- a/tests/test_image_vault_utils.cpp +++ b/tests/test_image_vault_utils.cpp @@ -136,7 +136,7 @@ TEST_F(TestImageVaultUtils, extract_file_will_delete_file) { auto decoder = [](const QString&, const QString&) {}; - EXPECT_CALL(mock_file_ops, remove(Property(&QFile::fileName, test_path))); + EXPECT_CALL(mock_file_ops, remove(MatcherCast(Property(&QFile::fileName, test_path)))); MP_IMAGE_VAULT_UTILS.extract_file(test_path, decoder, true); } @@ -152,7 +152,7 @@ TEST_F(TestImageVaultUtils, extract_file_wont_delete_file) ++calls; }; - EXPECT_CALL(mock_file_ops, remove(_)).Times(0); + EXPECT_CALL(mock_file_ops, remove(An())).Times(0); MP_IMAGE_VAULT_UTILS.extract_file(test_path, decoder, false); EXPECT_EQ(calls, 1); diff --git a/tests/test_sftpserver.cpp b/tests/test_sftpserver.cpp index e7bc8b1d6a..754174e8c1 100644 --- a/tests/test_sftpserver.cpp +++ b/tests/test_sftpserver.cpp @@ -1060,7 +1060,7 @@ TEST_F(SftpServer, rename_cannot_remove_target_fails) const auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, remove(_)).WillOnce(Return(false)); + EXPECT_CALL(*mock_file_ops, remove(An())).WillOnce(Return(false)); EXPECT_CALL(*mock_file_ops, ownerId(_)).WillRepeatedly([](const QFileInfo& file) { return file.ownerId(); }); EXPECT_CALL(*mock_file_ops, groupId(_)).WillRepeatedly([](const QFileInfo& file) { return file.groupId(); }); EXPECT_CALL(*mock_file_ops, exists(A())).WillRepeatedly([](const QFileInfo& file) { @@ -1100,7 +1100,7 @@ TEST_F(SftpServer, rename_failure_fails) const auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); - EXPECT_CALL(*mock_file_ops, rename(_, _)).WillOnce(Return(false)); + EXPECT_CALL(*mock_file_ops, rename(An(), _)).WillOnce(Return(false)); EXPECT_CALL(*mock_file_ops, ownerId(_)).WillRepeatedly([](const QFileInfo& file) { return file.ownerId(); }); EXPECT_CALL(*mock_file_ops, groupId(_)).WillRepeatedly([](const QFileInfo& file) { return file.groupId(); }); EXPECT_CALL(*mock_file_ops, exists(A())).WillRepeatedly([](const QFileInfo& file) { From e3ba0d1b2b859e75d73f2c4157aabea9f2b5fbff Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 21:24:40 +0300 Subject: [PATCH 43/76] [hyperv-virtdisk-snapshot] use MP_FILEOPS for filesystem --- .../hyperv_api/virtdisk/virtdisk_snapshot.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp index 7239dd1798..9a59cb34b8 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace { @@ -79,7 +80,7 @@ void VirtDiskSnapshot::capture_impl() // Check if head disk already exists. The head disk may not exist for a VM // that has no snapshots yet. - if (!std::filesystem::exists(head_path)) + if (!MP_FILEOPS.exists(head_path)) { const auto& parent = get_parent(); const auto& target = parent ? make_snapshot_path(*parent) : base_vhdx_path; @@ -87,7 +88,7 @@ void VirtDiskSnapshot::capture_impl() } // Step 1: Rename current head to snapshot name - std::filesystem::rename(head_path, snapshot_path); + MP_FILEOPS.rename(head_path, snapshot_path); // Step 2: Create a new head from the snapshot create_new_child_disk(snapshot_path, head_path); @@ -98,13 +99,13 @@ void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent { assert(virtdisk); // The parent must already exist. - if (!std::filesystem::exists(parent)) + if (!MP_FILEOPS.exists(parent)) throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::no_such_file_or_directory), "Parent disk `{}` does not exist", parent}; // The given child path must not exist - if (std::filesystem::exists(child)) + if (MP_FILEOPS.exists(child)) throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists), "Child disk `{}` already exists", child}; @@ -126,7 +127,7 @@ void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVis const std::filesystem::path& new_parent) const { // The parent must already exist. - if (!std::filesystem::exists(new_parent)) + if (!MP_FILEOPS.exists(new_parent)) throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::no_such_file_or_directory), "Parent disk `{}` does not exist", new_parent}; @@ -135,7 +136,7 @@ void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVis { const auto& child_path = make_snapshot_path(*child); - if (std::filesystem::exists(child_path)) + if (MP_FILEOPS.exists(child_path)) throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists), "Child disk `{}` already exists", child_path}; @@ -191,7 +192,7 @@ void VirtDiskSnapshot::erase_impl() } // Finally, erase the merged disk. mpl::debug(kLogCategory, "Removing snapshot file: `{}`", self_path); - std::filesystem::remove(self_path); + MP_FILEOPS.remove(self_path); } void VirtDiskSnapshot::apply_impl() @@ -203,7 +204,7 @@ void VirtDiskSnapshot::apply_impl() // Restoring a snapshot means we're discarding the head state. std::error_code ec{}; - std::filesystem::remove(head_path, ec); + MP_FILEOPS.remove(head_path, ec); mpl::debug(kLogCategory, "apply_impl() -> {} remove: {}", head_path, ec); // Create a new head from the snapshot From b155809af89f549bbbf4e172950baa6e53e221a8 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 6 May 2025 22:39:58 +0300 Subject: [PATCH 44/76] [hyperv-virtdisk] fix unit tests --- .../hyperv_api/hyperv_api_operation_result.h | 2 +- .../virtdisk/virtdisk_api_wrapper.cpp | 5 ++- tests/hyperv_api/test_ut_hyperv_virtdisk.cpp | 35 +++++++++---------- tests/mock_logger.cpp | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h index dd898da280..a0f5f08349 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h +++ b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h @@ -46,7 +46,7 @@ struct ResultCode [[nodiscard]] explicit operator bool() const noexcept { - return !FAILED(result); + return result == ERROR_SUCCESS; } [[nodiscard]] explicit operator HRESULT() const noexcept diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index 26f8d7bd80..13334bb832 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -27,6 +27,7 @@ #include #include +#include namespace multipass::hyperv::virtdisk { @@ -155,7 +156,7 @@ OperationResult VirtDiskWrapper::create_virtual_disk(const CreateVirtualDiskPara auto fill_target = [this](const std::wstring& predecessor_path, PCWSTR& target_path, VIRTUAL_STORAGE_TYPE& target_type) { std::filesystem::path pp{predecessor_path}; - if (!std::filesystem::exists(pp)) + if (!MP_FILEOPS.exists(pp)) { throw VirtDiskCreateError{"Predecessor VHDX file `{}` does not exist!", pp}; } @@ -298,8 +299,6 @@ OperationResult VirtDiskWrapper::resize_virtual_disk(const std::filesystem::path return OperationResult{NOERROR, L""}; } - // auto log_and_yield - mpl::error(kLogCategory, "resize_virtual_disk(...) > ResizeVirtualDisk failed with {}!", resize_result); return OperationResult{E_FAIL, fmt::format(L"ResizeVirtualDisk failed with {}!", resize_result)}; diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index 89d917accc..a0b4194301 100644 --- a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -17,6 +17,7 @@ #include "hyperv_test_utils.h" #include "tests/common.h" +#include "tests/mock_file_ops.h" #include "tests/mock_logger.h" #include @@ -115,7 +116,7 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_happy_path) ) { ASSERT_NE(nullptr, VirtualStorageType); - ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); ASSERT_NE(nullptr, Path); ASSERT_STREQ(Path, L"test.vhdx"); @@ -189,7 +190,7 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) ) { ASSERT_NE(nullptr, VirtualStorageType); - ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); ASSERT_NE(nullptr, Path); ASSERT_STREQ(Path, L"test.vhd"); @@ -246,6 +247,9 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) mock_api_table.GetVirtualDiskInformation = mock_get_virtual_disk_information.AsStdFunction(); mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); + auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); + EXPECT_CALL(*mock_file_ops, exists(TypedEq(L"source.vhdx"))).WillOnce(Return(true)); + /****************************************************** * Verify that the dependencies are called with right * data. @@ -261,11 +265,9 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) ULONG ProviderSpecificFlags, PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, LPOVERLAPPED Overlapped, - PHANDLE Handle - - ) { + PHANDLE Handle) { ASSERT_NE(nullptr, VirtualStorageType); - ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); ASSERT_NE(nullptr, Path); ASSERT_STREQ(Path, L"test.vhdx"); @@ -505,24 +507,19 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_open_failed) ******************************************************/ { EXPECT_CALL(mock_open_virtual_disk, Call) - .WillOnce(DoAll( - [](PVIRTUAL_STORAGE_TYPE VirtualStorageType, - PCWSTR Path, - VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, - OPEN_VIRTUAL_DISK_FLAG Flags, - POPEN_VIRTUAL_DISK_PARAMETERS Parameters, - PHANDLE Handle) { - - }, - Return(ERROR_PATH_NOT_FOUND))); - + .WillOnce(DoAll([](PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) {}, + Return(ERROR_PATH_NOT_FOUND))); logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "resize_virtual_disk(...) > vhdx_path: test.vhdx, new_size_bytes: 1234567"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: test.vhdx"); - logger_scope.mock_logger->expect_log(mpl::Level::error, - "open_virtual_disk(...) > OpenVirtualDisk failed with: 3"); + logger_scope.mock_logger->expect_log(mpl::Level::error, "open_virtual_disk(...) > OpenVirtualDisk failed with"); } { diff --git a/tests/mock_logger.cpp b/tests/mock_logger.cpp index 62054ed822..258d057fff 100644 --- a/tests/mock_logger.cpp +++ b/tests/mock_logger.cpp @@ -50,7 +50,7 @@ mpt::MockLogger::Scope::~Scope() void mpt::MockLogger::expect_log(mpl::Level lvl, const std::string& substr, const Cardinality& times) { - EXPECT_CALL(*this, log(lvl, _, HasSubstr(substr))).Times(times); + EXPECT_CALL(*this, log(lvl, _, HasSubstr(substr))).Times(times).Description(substr); } void mpt::MockLogger::screen_logs(mpl::Level lvl) From 917223ed8ea34f66226491bf0b4c1c7eb61a4e8d Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 11:45:04 +0300 Subject: [PATCH 45/76] [hyperv-virtdisk-ut] add unit tests for merge & reparent Also, simplify boilerplate code by wrapping MockFuncion with StrictMock which allows us to eliminate the need for EXPECT_NO_CALL stubs. --- tests/hyperv_api/test_ut_hyperv_virtdisk.cpp | 341 +++++++++++-------- 1 file changed, 201 insertions(+), 140 deletions(-) diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index a0b4194301..3a6a7f5973 100644 --- a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -39,64 +39,57 @@ struct HyperVVirtDisk_UnitTests : public ::testing::Test { mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); + template + using StrictMockFunction = ::testing::StrictMock<::testing::MockFunction>; + virtual void SetUp() override { - - // Each of the unit tests are expected to have their own mock functions - // and override the mock_api_table with them. Hence, the stub mocks should - // not be called at all. - // If any of them do get called, then: - // - // a-) You have forgotten to mock something - // b-) The implementation is using a function that you didn't expect - // - // Either way, you should have a look. - EXPECT_NO_CALL(stub_mock_create_virtual_disk); - EXPECT_NO_CALL(stub_mock_open_virtual_disk); - EXPECT_NO_CALL(stub_mock_resize_virtual_disk); - EXPECT_NO_CALL(stub_mock_get_virtual_disk_information); - EXPECT_NO_CALL(stub_mock_close_handle); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); } // Set of placeholder mocks in order to catch *unexpected* calls. - ::testing::MockFunction stub_mock_create_virtual_disk; - ::testing::MockFunction stub_mock_open_virtual_disk; - ::testing::MockFunction stub_mock_resize_virtual_disk; - ::testing::MockFunction stub_mock_merge_virtual_disk; - ::testing::MockFunction stub_mock_get_virtual_disk_information; - ::testing::MockFunction stub_mock_set_virtual_disk_information; - ::testing::MockFunction stub_mock_close_handle; + StrictMockFunction mock_create_virtual_disk; + StrictMockFunction mock_open_virtual_disk; + StrictMockFunction mock_resize_virtual_disk; + StrictMockFunction mock_merge_virtual_disk; + StrictMockFunction mock_get_virtual_disk_information; + StrictMockFunction mock_set_virtual_disk_information; + StrictMockFunction mock_close_handle; // Initialize the API table with stub functions, so if any of these fire without // our will, we'll know. hyperv::virtdisk::VirtDiskAPITable mock_api_table{ - stub_mock_create_virtual_disk.AsStdFunction(), - stub_mock_open_virtual_disk.AsStdFunction(), - stub_mock_resize_virtual_disk.AsStdFunction(), - stub_mock_merge_virtual_disk.AsStdFunction(), - stub_mock_get_virtual_disk_information.AsStdFunction(), - stub_mock_set_virtual_disk_information.AsStdFunction(), - stub_mock_close_handle.AsStdFunction(), + mock_create_virtual_disk.AsStdFunction(), + mock_open_virtual_disk.AsStdFunction(), + mock_resize_virtual_disk.AsStdFunction(), + mock_merge_virtual_disk.AsStdFunction(), + mock_get_virtual_disk_information.AsStdFunction(), + mock_set_virtual_disk_information.AsStdFunction(), + mock_close_handle.AsStdFunction(), }; // Sentinel values as mock API parameters. These handles are opaque handles and // they're not being dereferenced in any way -- only address values are compared. inline static auto mock_handle_object = reinterpret_cast(0xbadf00d); + + void open_vhd_expect_failure() + { + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + + EXPECT_CALL(mock_open_virtual_disk, Call).WillOnce(Return(ERROR_PATH_NOT_FOUND)); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path:"); + logger_scope.mock_logger->expect_log(mpl::Level::error, + "open_virtual_disk(...) > OpenVirtualDisk failed with:"); + } }; // --------------------------------------------------------- TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_happy_path) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_create_virtual_disk; - ::testing::MockFunction mock_close_handle; - - mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -112,9 +105,7 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_happy_path) ULONG ProviderSpecificFlags, PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, LPOVERLAPPED Overlapped, - PHANDLE Handle - - ) { + PHANDLE Handle) { ASSERT_NE(nullptr, VirtualStorageType); ASSERT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_VHDX); ASSERT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); @@ -140,7 +131,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_happy_path) EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "create_virtual_disk(...) > params: Size (in bytes): (2097152) | Path: (test.vhdx)"); @@ -162,15 +152,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_happy_path) TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_create_virtual_disk; - ::testing::MockFunction mock_close_handle; - - mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -212,7 +193,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "create_virtual_disk(...) > params: Size (in bytes): (2097152) | Path: (test.vhd)"); @@ -234,19 +214,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_create_virtual_disk; - ::testing::MockFunction mock_open_virtual_disk; - ::testing::MockFunction mock_get_virtual_disk_information; - ::testing::MockFunction mock_close_handle; - - mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - mock_api_table.GetVirtualDiskInformation = mock_get_virtual_disk_information.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); EXPECT_CALL(*mock_file_ops, exists(TypedEq(L"source.vhdx"))).WillOnce(Return(true)); @@ -341,7 +308,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).Times(2).WillRepeatedly(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "create_virtual_disk(...) > params: Size (in bytes): (0) | Path: (test.vhdx)"); @@ -369,13 +335,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_create_virtual_disk; - - mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -395,7 +354,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) ) {}, Return(ERROR_PATH_NOT_FOUND))); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "create_virtual_disk(...) > params: Size (in bytes): (2097152) | Path: (test.vhd)"); @@ -420,17 +378,6 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_happy_path) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_open_virtual_disk; - ::testing::MockFunction mock_resize_virtual_disk; - ::testing::MockFunction mock_close_handle; - - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - mock_api_table.ResizeVirtualDisk = mock_resize_virtual_disk.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -454,7 +401,6 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_happy_path) ASSERT_EQ(nullptr, Parameters); ASSERT_NE(nullptr, Handle); ASSERT_EQ(nullptr, *Handle); - *Handle = mock_handle_object; }, Return(ERROR_SUCCESS))); @@ -475,7 +421,6 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_happy_path) Return(ERROR_SUCCESS))); EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "resize_virtual_disk(...) > vhdx_path: test.vhdx, new_size_bytes: 1234567"); @@ -494,32 +439,15 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_happy_path) TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_open_failed) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_open_virtual_disk; - - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. ******************************************************/ { - EXPECT_CALL(mock_open_virtual_disk, Call) - .WillOnce(DoAll([](PVIRTUAL_STORAGE_TYPE VirtualStorageType, - PCWSTR Path, - VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, - OPEN_VIRTUAL_DISK_FLAG Flags, - POPEN_VIRTUAL_DISK_PARAMETERS Parameters, - PHANDLE Handle) {}, - Return(ERROR_PATH_NOT_FOUND))); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); + open_vhd_expect_failure(); logger_scope.mock_logger->expect_log( mpl::Level::debug, "resize_virtual_disk(...) > vhdx_path: test.vhdx, new_size_bytes: 1234567"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: test.vhdx"); - logger_scope.mock_logger->expect_log(mpl::Level::error, "open_virtual_disk(...) > OpenVirtualDisk failed with"); } { @@ -535,16 +463,6 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_open_failed) TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_resize_failed) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_open_virtual_disk; - ::testing::MockFunction mock_resize_virtual_disk; - ::testing::MockFunction mock_close_handle; - - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - mock_api_table.ResizeVirtualDisk = mock_resize_virtual_disk.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); /****************************************************** * Verify that the dependencies are called with right @@ -568,7 +486,6 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_resize_failed) Return(ERROR_INVALID_PARAMETER))); EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log( mpl::Level::debug, "resize_virtual_disk(...) > vhdx_path: test.vhdx, new_size_bytes: 1234567"); @@ -590,17 +507,6 @@ TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_resize_failed) TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_happy_path) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_open_virtual_disk; - ::testing::MockFunction mock_get_virtual_disk_information; - ::testing::MockFunction mock_close_handle; - - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - mock_api_table.GetVirtualDiskInformation = mock_get_virtual_disk_information.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -693,7 +599,6 @@ TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_happy_path) Return(ERROR_SUCCESS))); EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_virtual_disk_info(...) > vhdx_path: test.vhdx"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: test.vhdx"); } @@ -725,17 +630,6 @@ TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_happy_path) TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_fail_some) { - /****************************************************** - * Override the default mock functions. - ******************************************************/ - ::testing::MockFunction mock_open_virtual_disk; - ::testing::MockFunction mock_get_virtual_disk_information; - ::testing::MockFunction mock_close_handle; - - mock_api_table.OpenVirtualDisk = mock_open_virtual_disk.AsStdFunction(); - mock_api_table.GetVirtualDiskInformation = mock_get_virtual_disk_information.AsStdFunction(); - mock_api_table.CloseHandle = mock_close_handle.AsStdFunction(); - /****************************************************** * Verify that the dependencies are called with right * data. @@ -801,7 +695,6 @@ TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_fail_some) .WillOnce(Return(ERROR_INVALID_PARAMETER)); EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "VirtDiskWrapper::VirtDiskWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "get_virtual_disk_info(...) > vhdx_path: test.vhdx"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: test.vhdx"); logger_scope.mock_logger->expect_log(mpl::Level::warning, "get_virtual_disk_info(...) > failed to get 6"); @@ -829,4 +722,172 @@ TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_fail_some) } } +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, reparent_virtual_disk_happy_path) +{ + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + + EXPECT_CALL(mock_open_virtual_disk, Call) + .WillOnce(DoAll( + [](PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) { + ASSERT_NE(nullptr, VirtualStorageType); + EXPECT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + EXPECT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); + EXPECT_STREQ(Path, L"child.avhdx"); + EXPECT_EQ(VIRTUAL_DISK_ACCESS_NONE, VirtualDiskAccessMask); + EXPECT_EQ(OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS, Flags); + ASSERT_NE(nullptr, Parameters); + EXPECT_EQ(Parameters->Version, OPEN_VIRTUAL_DISK_VERSION_2); + EXPECT_EQ(Parameters->Version2.GetInfoOnly, false); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_set_virtual_disk_information, Call) + .WillOnce(DoAll( + [](HANDLE VirtualDiskHandle, PSET_VIRTUAL_DISK_INFO VirtualDiskInfo) { + ASSERT_EQ(VirtualDiskHandle, mock_handle_object); + ASSERT_NE(nullptr, VirtualDiskInfo); + EXPECT_EQ(VirtualDiskInfo->Version, SET_VIRTUAL_DISK_INFO_PARENT_PATH_WITH_DEPTH); + EXPECT_STREQ(VirtualDiskInfo->ParentPathWithDepthInfo.ParentFilePath, L"parent.vhdx"); + EXPECT_EQ(VirtualDiskInfo->ParentPathWithDepthInfo.ChildDepth, 1); + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); + logger_scope.mock_logger->expect_log( + mpl::Level::debug, + "reparent_virtual_disk(...) > child: child.avhdx, new parent: parent.vhdx"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: child.avhdx"); + } + + { + uut_t uut{mock_api_table}; + const auto& [status, status_msg] = uut.reparent_virtual_disk("child.avhdx", "parent.vhdx"); + EXPECT_TRUE(status); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, reparent_virtual_disk_open_disk_failure) +{ + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + open_vhd_expect_failure(); + logger_scope.mock_logger->expect_log( + mpl::Level::debug, + "reparent_virtual_disk(...) > child: child.avhdx, new parent: parent.vhdx"); + } + + { + uut_t uut{mock_api_table}; + const auto& [status, status_msg] = uut.reparent_virtual_disk("child.avhdx", "parent.vhdx"); + EXPECT_FALSE(status); + EXPECT_FALSE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, merge_virtual_disk_happy_path) +{ + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + + EXPECT_CALL(mock_open_virtual_disk, Call) + .WillOnce(DoAll( + [](PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) { + ASSERT_NE(nullptr, VirtualStorageType); + EXPECT_EQ(VirtualStorageType->DeviceId, VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN); + EXPECT_EQ(VirtualStorageType->VendorId, VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); + EXPECT_STREQ(Path, L"child.avhdx"); + EXPECT_EQ(VIRTUAL_DISK_ACCESS_METAOPS | VIRTUAL_DISK_ACCESS_GET_INFO, VirtualDiskAccessMask); + EXPECT_EQ(OPEN_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_NE(nullptr, Parameters); + EXPECT_EQ(Parameters->Version, OPEN_VIRTUAL_DISK_VERSION_1); + EXPECT_EQ(Parameters->Version1.RWDepth, 2); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_merge_virtual_disk, Call) + .WillOnce(DoAll( + [](HANDLE VirtualDiskHandle, + MERGE_VIRTUAL_DISK_FLAG Flags, + PMERGE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) { + ASSERT_EQ(VirtualDiskHandle, mock_handle_object); + EXPECT_EQ(MERGE_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_NE(nullptr, Parameters); + EXPECT_EQ(MERGE_VIRTUAL_DISK_VERSION_1, Parameters->Version); + EXPECT_EQ(MERGE_VIRTUAL_DISK_DEFAULT_MERGE_DEPTH, Parameters->Version1.MergeDepth); + ASSERT_EQ(nullptr, Overlapped); + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_close_handle, Call(Eq(mock_handle_object))).WillOnce(Return(true)); + logger_scope.mock_logger->expect_log(mpl::Level::debug, + "merge_virtual_disk_to_parent(...) > child: child.avhdx"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_virtual_disk(...) > vhdx_path: child.avhdx"); + } + + { + uut_t uut{mock_api_table}; + const auto& [status, status_msg] = uut.merge_virtual_disk_to_parent("child.avhdx"); + EXPECT_TRUE(status); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, merge_virtual_disk_open_disk_failure) +{ + + /****************************************************** + * Verify that the dependencies are called with right + * data. + ******************************************************/ + { + open_vhd_expect_failure(); + logger_scope.mock_logger->expect_log(mpl::Level::debug, + "merge_virtual_disk_to_parent(...) > child: child.avhdx"); + } + + { + uut_t uut{mock_api_table}; + const auto& [status, status_msg] = uut.merge_virtual_disk_to_parent("child.avhdx"); + EXPECT_FALSE(status); + EXPECT_FALSE(status_msg.empty()); + } +} + } // namespace multipass::test From 276e2453d6ff5fc2428f003d30724c6da7399c7a Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 15:12:41 +0300 Subject: [PATCH 46/76] [hyperv-virtdisk-it] add integration tests for merge & reparent [hyperv_test_utils] improve make_tempfile_path --- .../hyperv_api/virtdisk/virtdisk_disk_info.h | 1 - tests/hyperv_api/hyperv_test_utils.h | 28 ++- tests/hyperv_api/test_it_hyperv_virtdisk.cpp | 171 +++++++++++++----- tests/mock_logger.cpp | 7 +- 4 files changed, 151 insertions(+), 56 deletions(-) diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h index b967966a36..c38a37a0e1 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h @@ -29,7 +29,6 @@ namespace multipass::hyperv::virtdisk struct VirtualDiskInfo { - struct size_info { std::uint64_t virtual_{}; diff --git a/tests/hyperv_api/hyperv_test_utils.h b/tests/hyperv_api/hyperv_test_utils.h index 117cf3ea4d..e9abcad2a0 100644 --- a/tests/hyperv_api/hyperv_test_utils.h +++ b/tests/hyperv_api/hyperv_test_utils.h @@ -20,6 +20,9 @@ #include #include +#include + +#include #define EXPECT_NO_CALL(mock) EXPECT_CALL(mock, Call).Times(0) @@ -34,9 +37,12 @@ inline auto trim_whitespace(const CharT* input) return str; } +/** + * Create an unique path for a temporary file. + */ inline auto make_tempfile_path(std::string extension) { - + static std::mt19937_64 rng{std::random_device{}()}; struct auto_remove_path { @@ -61,13 +67,23 @@ inline auto make_tempfile_path(std::string extension) private: const std::filesystem::path path; }; - char pattern[] = "temp-XXXXXX"; - if (_mktemp_s(pattern) != 0) + + std::filesystem::path temp_path{}; + std::uint32_t remaining_attempts = 10; + do { - throw std::runtime_error{"Incorrect format for _mktemp_s."}; + temp_path = std::filesystem::temp_directory_path() / fmt::format("temp-{:016x}{}", rng(), extension); + // The generated path is vulnerable to TOCTOU, but it's highly unlikely we'll see a clash. + // Better handling of this would require creation of a placeholder file, and an atomic swap + // with the real file. + } while (std::filesystem::exists(temp_path) && --remaining_attempts); + + if (!remaining_attempts) + { + throw std::runtime_error{"Exhausted attempt count for temporary filename generation."}; } - const auto filename = pattern + extension; - return auto_remove_path{std::filesystem::temp_directory_path() / filename}; + + return auto_remove_path{temp_path}; } } // namespace multipass::test diff --git a/tests/hyperv_api/test_it_hyperv_virtdisk.cpp b/tests/hyperv_api/test_it_hyperv_virtdisk.cpp index a55e7256a4..a52dc12a0e 100644 --- a/tests/hyperv_api/test_it_hyperv_virtdisk.cpp +++ b/tests/hyperv_api/test_it_hyperv_virtdisk.cpp @@ -28,6 +28,11 @@ namespace multipass::test { +/** + * 16 MiB + */ +constexpr static auto kTestVhdxSize = 1024 * 1024 * 16ULL; + using uut_t = hyperv::virtdisk::VirtDiskWrapper; struct HyperVVirtDisk_IntegrationTests : public ::testing::Test @@ -42,7 +47,7 @@ TEST_F(HyperVVirtDisk_IntegrationTests, create_virtual_disk_vhdx) uut_t uut{}; hyperv::virtdisk::CreateVirtualDiskParameters params{}; params.path = temp_path; - params.size_in_bytes = 1024 * 1024 * 1024; // 1 GiB + params.size_in_bytes = kTestVhdxSize; const auto result = uut.create_virtual_disk(params); ASSERT_TRUE(result); @@ -57,7 +62,7 @@ TEST_F(HyperVVirtDisk_IntegrationTests, create_virtual_disk_vhd) uut_t uut{}; hyperv::virtdisk::CreateVirtualDiskParameters params{}; params.path = temp_path; - params.size_in_bytes = 1024 * 1024 * 1024; // 1 GiB + params.size_in_bytes = kTestVhdxSize; const auto result = uut.create_virtual_disk(params); ASSERT_TRUE(result); @@ -72,7 +77,7 @@ TEST_F(HyperVVirtDisk_IntegrationTests, get_virtual_disk_properties) uut_t uut{}; hyperv::virtdisk::CreateVirtualDiskParameters params{}; params.path = temp_path; - params.size_in_bytes = 1024 * 1024 * 1024; // 1 GiB + params.size_in_bytes = kTestVhdxSize; const auto c_result = uut.create_virtual_disk(params); ASSERT_TRUE(c_result); @@ -85,7 +90,7 @@ TEST_F(HyperVVirtDisk_IntegrationTests, get_virtual_disk_properties) ASSERT_TRUE(info.size.has_value()); ASSERT_STREQ(info.virtual_storage_type.value().c_str(), "vhdx"); - ASSERT_EQ(info.size->virtual_, 1024 * 1024 * 1024); + ASSERT_EQ(info.size->virtual_, params.size_in_bytes); ASSERT_EQ(info.size->block, 1024 * 1024); ASSERT_EQ(info.size->sector, 512); @@ -100,7 +105,7 @@ TEST_F(HyperVVirtDisk_IntegrationTests, resize_grow) uut_t uut{}; hyperv::virtdisk::CreateVirtualDiskParameters params{}; params.path = temp_path; - params.size_in_bytes = 1024 * 1024 * 1024; // 1 GiB + params.size_in_bytes = kTestVhdxSize; const auto c_result = uut.create_virtual_disk(params); ASSERT_TRUE(c_result); @@ -139,53 +144,123 @@ TEST_F(HyperVVirtDisk_IntegrationTests, resize_grow) fmt::print("{}", info); } -TEST_F(HyperVVirtDisk_IntegrationTests, DISABLED_resize_shrink) +TEST_F(HyperVVirtDisk_IntegrationTests, create_child_disk) { - auto temp_path = make_tempfile_path(".vhdx"); - std::wprintf(L"Path: %s\n", static_cast(temp_path).c_str()); - uut_t uut{}; - hyperv::virtdisk::CreateVirtualDiskParameters params{}; - params.path = temp_path; - params.size_in_bytes = 1024 * 1024 * 1024; // 1 GiB - - const auto c_result = uut.create_virtual_disk(params); - ASSERT_TRUE(c_result); - ASSERT_TRUE(c_result.status_msg.empty()); - - hyperv::virtdisk::VirtualDiskInfo info{}; - const auto g_result = uut.get_virtual_disk_info(temp_path, info); - - ASSERT_TRUE(g_result); - ASSERT_TRUE(info.virtual_storage_type.has_value()); - ASSERT_TRUE(info.size.has_value()); - - ASSERT_STREQ(info.virtual_storage_type.value().c_str(), "vhdx"); - ASSERT_EQ(info.size->virtual_, params.size_in_bytes); - ASSERT_EQ(info.size->block, 1024 * 1024); - ASSERT_EQ(info.size->sector, 512); - - fmt::print("{}", info); - - const auto r_result = uut.resize_virtual_disk(temp_path, params.size_in_bytes / 2); - ASSERT_TRUE(r_result); - - info = {}; - - // SmallestSafeVirtualSize - - const auto g2_result = uut.get_virtual_disk_info(temp_path, info); - - ASSERT_TRUE(g2_result); - ASSERT_TRUE(info.virtual_storage_type.has_value()); - ASSERT_TRUE(info.size.has_value()); + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", static_cast(parent_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.path = parent_temp_path; + params.size_in_bytes = kTestVhdxSize; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + // Create child + auto child_temp_path = make_tempfile_path(".avhdx"); + std::wprintf(L"Child Path: %s\n", static_cast(child_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.predecessor = hyperv::virtdisk::ParentPathParameters{parent_temp_path}; + params.path = child_temp_path; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } +} - ASSERT_STREQ(info.virtual_storage_type.value().c_str(), "vhdx"); - ASSERT_EQ(info.size->virtual_, params.size_in_bytes / 2); - ASSERT_EQ(info.size->block, 1024 * 1024); - ASSERT_EQ(info.size->sector, 512); +TEST_F(HyperVVirtDisk_IntegrationTests, merge_virtual_disk) +{ + uut_t uut{}; + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", static_cast(parent_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.path = parent_temp_path; + params.size_in_bytes = kTestVhdxSize; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + // Create child + auto child_temp_path = make_tempfile_path(".avhdx"); + std::wprintf(L"Child Path: %s\n", static_cast(child_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.predecessor = hyperv::virtdisk::ParentPathParameters{parent_temp_path}; + params.path = child_temp_path; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Merge child to parent + const auto result = uut.merge_virtual_disk_to_parent(child_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); +} - fmt::print("{}", info); +TEST_F(HyperVVirtDisk_IntegrationTests, merge_reparent_virtual_disk) +{ + uut_t uut{}; + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", static_cast(parent_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.path = parent_temp_path; + params.size_in_bytes = kTestVhdxSize; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + // Create child + auto child_temp_path = make_tempfile_path(".avhdx"); + std::wprintf(L"Child Path: %s\n", static_cast(child_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.predecessor = hyperv::virtdisk::ParentPathParameters{parent_temp_path}; + params.path = child_temp_path; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Create grandchild + auto grandchild_temp_path = make_tempfile_path(".avhdx"); + std::wprintf(L"Grandchild Path: %s\n", static_cast(grandchild_temp_path).c_str()); + { + hyperv::virtdisk::CreateVirtualDiskParameters params{}; + params.predecessor = hyperv::virtdisk::ParentPathParameters{child_temp_path}; + params.path = grandchild_temp_path; + + const auto result = uut.create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Merge child to parent + { + const auto result = uut.merge_virtual_disk_to_parent(child_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Reparent grandchild to parent + { + const auto result = uut.reparent_virtual_disk(grandchild_temp_path, parent_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } } } // namespace multipass::test diff --git a/tests/mock_logger.cpp b/tests/mock_logger.cpp index 258d057fff..ba4e3b0b4e 100644 --- a/tests/mock_logger.cpp +++ b/tests/mock_logger.cpp @@ -50,7 +50,12 @@ mpt::MockLogger::Scope::~Scope() void mpt::MockLogger::expect_log(mpl::Level lvl, const std::string& substr, const Cardinality& times) { - EXPECT_CALL(*this, log(lvl, _, HasSubstr(substr))).Times(times).Description(substr); + EXPECT_CALL(*this, log(lvl, _, HasSubstr(substr))) + .Times(times) + .Description(fmt::format("level: {}, substr: `{}`, times: {}", + logging::as_string(lvl), + substr, + times.ConservativeLowerBound())); } void mpt::MockLogger::screen_logs(mpl::Level lvl) From 70a2a1dfea8facdb8372148758f84176fba9bb9e Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:02:44 +0300 Subject: [PATCH 47/76] [hyperv-api] grooming & add more logs --- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 2 +- .../hyperv_api/hcn/hyperv_hcn_ipam.cpp | 15 ++++++------ .../hcn/hyperv_hcn_network_policy.cpp | 23 ++++++++++--------- ...hyperv_hcn_network_policy_netadaptername.h | 18 --------------- .../hyperv_api/hcn/hyperv_hcn_route.cpp | 17 +++++++------- .../hyperv_api/hcn/hyperv_hcn_subnet.cpp | 15 ++++++------ .../hcs_virtual_machine_factory.cpp | 1 - .../hyperv_api/virtdisk/virtdisk_snapshot.cpp | 10 +++++++- .../hyperv_api/virtdisk/virtdisk_snapshot.h | 1 + 9 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 97a192361c..0f37b877d0 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -212,7 +212,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params kLogCategory, "HCNWrapper::create_network(...) > HcnCreateNetwork failed with {}: {}", result.code, - std::system_category().message(static_cast(result.code))); + static_cast(result.code)); } [[maybe_unused]] UniqueHcnNetwork _{network, api.CloseNetwork}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp index c146246fb5..f54be22473 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp @@ -19,12 +19,12 @@ #include -namespace hv = multipass::hyperv; -namespace hcn = hv::hcn; +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnIpam; template template -auto fmt::formatter::format(const hcn::HcnIpam& ipam, FormatContext& ctx) const -> +auto fmt::formatter::format(const HcnIpam& ipam, FormatContext& ctx) const -> typename FormatContext::iterator { constexpr static auto subnet_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( @@ -40,14 +40,13 @@ auto fmt::formatter::format(const hcn::HcnIpam& ipam, Format return format_to(ctx.out(), subnet_template.as(), - hv::maybe_widen{ipam.type}, + maybe_widen{ipam.type}, fmt::join(ipam.subnets, comma.as())); } -template auto fmt::formatter::format(const hcn::HcnIpam&, - fmt::format_context&) const +template auto fmt::formatter::format(const HcnIpam&, fmt::format_context&) const -> fmt::format_context::iterator; -template auto fmt::formatter::format(const hcn::HcnIpam&, - fmt::wformat_context&) const +template auto fmt::formatter::format(const HcnIpam&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp index f84785e305..e9815bf75c 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp @@ -19,26 +19,27 @@ #include #include -namespace hv = multipass::hyperv; -namespace hcn = hv::hcn; +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnNetworkPolicy; +using multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName; template struct NetworkPolicySettingsFormatters { - auto operator()(const hcn::HcnNetworkPolicyNetAdapterName& policy) + auto operator()(const HcnNetworkPolicyNetAdapterName& policy) { constexpr static auto netadaptername_settings_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( "NetworkAdapterName": "{}" )json"); - return fmt::format(netadaptername_settings_template.as(), hv::maybe_widen{policy.net_adapter_name}); + return fmt::format(netadaptername_settings_template.as(), maybe_widen{policy.net_adapter_name}); } }; template template -auto fmt::formatter::format(const hcn::HcnNetworkPolicy& policy, FormatContext& ctx) const - -> typename FormatContext::iterator +auto fmt::formatter::format(const HcnNetworkPolicy& policy, FormatContext& ctx) const -> + typename FormatContext::iterator { constexpr static auto route_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( {{ @@ -51,14 +52,14 @@ auto fmt::formatter::format(const hcn::HcnNetworkPo return format_to(ctx.out(), route_template.as(), - hv::maybe_widen{policy.type}, + maybe_widen{policy.type}, std::visit(NetworkPolicySettingsFormatters{}, policy.settings)); } -template auto fmt::formatter::format(const hcn::HcnNetworkPolicy&, - fmt::format_context&) const +template auto fmt::formatter::format(const HcnNetworkPolicy&, + fmt::format_context&) const -> fmt::format_context::iterator; -template auto fmt::formatter::format(const hcn::HcnNetworkPolicy&, - fmt::wformat_context&) const +template auto fmt::formatter::format(const HcnNetworkPolicy&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h index 50548e542c..75afa605de 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h @@ -32,22 +32,4 @@ struct HcnNetworkPolicyNetAdapterName } // namespace multipass::hyperv::hcn -/** - * Formatter type specialization for HcnNetworkPolicyNetAdapterName - */ -template -struct fmt::formatter -{ - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - - template - auto format(const multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName& policy, FormatContext& ctx) const - { - return format_to(ctx.out(), "NetAdapterName: ({})", policy.net_adapter_name); - } -}; - #endif diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp index c7f7211641..94e6b937b6 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp @@ -19,12 +19,12 @@ #include -namespace hv = multipass::hyperv; -namespace hcn = hv::hcn; +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnRoute; template template -auto fmt::formatter::format(const hcn::HcnRoute& route, FormatContext& ctx) const -> +auto fmt::formatter::format(const HcnRoute& route, FormatContext& ctx) const -> typename FormatContext::iterator { constexpr static auto route_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( @@ -36,15 +36,14 @@ auto fmt::formatter::format(const hcn::HcnRoute& route, For return format_to(ctx.out(), route_template.as(), - hv::maybe_widen{route.next_hop}, - hv::maybe_widen(route.destination_prefix), + maybe_widen{route.next_hop}, + maybe_widen(route.destination_prefix), route.metric); } -template auto fmt::formatter::format(const hcn::HcnRoute&, - fmt::format_context&) const +template auto fmt::formatter::format(const HcnRoute&, fmt::format_context&) const -> fmt::format_context::iterator; -template auto fmt::formatter::format(const hcn::HcnRoute&, - fmt::wformat_context&) const +template auto fmt::formatter::format(const HcnRoute&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp index 5dc039d5c1..97be8a34e0 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp @@ -19,12 +19,12 @@ #include -namespace hv = multipass::hyperv; -namespace hcn = hv::hcn; +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnSubnet; template template -auto fmt::formatter::format(const hcn::HcnSubnet& subnet, FormatContext& ctx) const -> +auto fmt::formatter::format(const HcnSubnet& subnet, FormatContext& ctx) const -> typename FormatContext::iterator { constexpr static auto subnet_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( @@ -43,13 +43,12 @@ auto fmt::formatter::format(const hcn::HcnSubnet& subnet, return format_to(ctx.out(), subnet_template.as(), fmt::join(subnet.routes, comma.as()), - hv::maybe_widen{subnet.ip_address_prefix}); + maybe_widen{subnet.ip_address_prefix}); } -template auto fmt::formatter::format(const hcn::HcnSubnet&, - fmt::format_context&) const +template auto fmt::formatter::format(const HcnSubnet&, fmt::format_context&) const -> fmt::format_context::iterator; -template auto fmt::formatter::format(const hcn::HcnSubnet&, - fmt::wformat_context&) const +template auto fmt::formatter::format(const HcnSubnet&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 82c495ead4..862b606fc9 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -214,7 +214,6 @@ std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceI network_params.name = bridge_name; network_params.type = hcn::HcnNetworkType::Transparent(); network_params.guid = multipass::utils::make_uuid(network_params.name).toStdString(); - hcn::HcnNetworkPolicy policy{hcn::HcnNetworkPolicyType::NetAdapterName(), hcn::HcnNetworkPolicyNetAdapterName{intf.id}}; network_params.policies.push_back(policy); diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp index 9a59cb34b8..f109a192e7 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -20,9 +20,9 @@ #include #include +#include #include #include -#include namespace { @@ -77,6 +77,7 @@ void VirtDiskSnapshot::capture_impl() const auto& head_path = base_vhdx_path.parent_path() / head_disk_name(); const auto& snapshot_path = make_snapshot_path(*this); + mpl::debug(kLogCategory, "capture_impl() -> head_path: {}, snapshot_path: {}", head_path, snapshot_path); // Check if head disk already exists. The head disk may not exist for a VM // that has no snapshots yet. @@ -97,6 +98,7 @@ void VirtDiskSnapshot::capture_impl() void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent, const std::filesystem::path& child) const { + mpl::debug(kLogCategory, "create_new_child_disk() -> parent: {}, child: {}", parent, child); assert(virtdisk); // The parent must already exist. if (!MP_FILEOPS.exists(parent)) @@ -126,6 +128,11 @@ void VirtDiskSnapshot::create_new_child_disk(const std::filesystem::path& parent void VirtDiskSnapshot::reparent_snapshot_disks(const VirtualMachine::SnapshotVista& snapshots, const std::filesystem::path& new_parent) const { + mpl::debug(kLogCategory, + "reparent_snapshot_disks() -> snapshots_count: {}, new_parent: {}", + snapshots.size(), + new_parent); + // The parent must already exist. if (!MP_FILEOPS.exists(new_parent)) throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::no_such_file_or_directory), @@ -158,6 +165,7 @@ void VirtDiskSnapshot::erase_impl() assert(virtdisk); const auto& parent = get_parent(); const auto& self_path = make_snapshot_path(*this); + mpl::debug(kLogCategory, "erase_impl() -> parent: {}, self_path: {}", parent->get_name(), self_path); // 1: Merge this to its parent if (const auto merge_r = virtdisk->merge_virtual_disk_to_parent(self_path); merge_r) diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h index 82a35f57e5..e492e2070b 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h @@ -22,6 +22,7 @@ #include #include + #include namespace multipass::hyperv::virtdisk From 2b26d9d076443a259938c5826052c3e756482caf Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:15:45 +0300 Subject: [PATCH 48/76] [hyperv-api] replace string_to_wstring with maybe_widen --- include/multipass/platform_win.h | 8 ----- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 8 ++--- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 33 ++++++++++--------- src/platform/platform_win.cpp | 12 ++----- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/include/multipass/platform_win.h b/include/multipass/platform_win.h index 6bd76c81a8..c80239a752 100644 --- a/include/multipass/platform_win.h +++ b/include/multipass/platform_win.h @@ -91,14 +91,6 @@ struct wsa_init_wrapper // --------------------------------------------------------- -/** - * Convert a multi-byte string to a wide-character string. - * - * @param str Multi-byte string - * @return Wide-character equivalent of the given multi-byte string. - */ -[[nodiscard]] auto string_to_wstring(const std::string& str) -> std::wstring; - } // namespace multipass::platform #endif // MULTIPASS_PLATFORM_WIN_H diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 0f37b877d0..9fb1872906 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -193,8 +193,8 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params // Render the template const auto network_settings = fmt::format(network_settings_template, - fmt::arg(L"Name", string_to_wstring(params.name)), - fmt::arg(L"Type", string_to_wstring(std::string{params.type})), + fmt::arg(L"Name", maybe_widen{params.name}), + fmt::arg(L"Type", maybe_widen{std::string{params.type}}), fmt::arg(L"Flags", fmt::underlying(params.flags)), fmt::arg(L"Ipams", fmt::join(params.ipams, L",")), fmt::arg(L"Policies", fmt::join(params.policies, L","))); @@ -257,9 +257,9 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para // Render the template const auto endpoint_settings = fmt::format( endpoint_settings_template, - fmt::arg(L"HostComputeNetwork", string_to_wstring(params.network_guid)), + fmt::arg(L"HostComputeNetwork", maybe_widen{params.network_guid}), fmt::arg(L"MacAddress", - params.mac_address ? fmt::format(L"\"{}\"", string_to_wstring(params.mac_address.value())) : L"null")); + params.mac_address ? fmt::format(L"\"{}\"", maybe_widen{params.mac_address.value()}) : L"null")); HCN_ENDPOINT endpoint{nullptr}; const auto result = perform_hcn_operation(api, api.CreateEndpoint, diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 66f2e4e0c7..06123e3e05 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -15,6 +15,7 @@ * */ +#include "hyperv_api/hyperv_api_string_conversion.h" #include #include #include @@ -123,7 +124,7 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri mpl::debug(kLogCategory, "open_host_compute_system(...) > name: ({})", name); // Windows API uses wide strings. - const auto name_w = string_to_wstring(name); + const std::wstring name_w = maybe_widen{name}; constexpr auto kRequestedAccessLevel = GENERIC_ALL; HCS_SYSTEM system{nullptr}; @@ -270,8 +271,8 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam for (const auto& endpoint : params.endpoints) { network_adapters.push_back(fmt::format(network_adapter_template, - string_to_wstring(endpoint.endpoint_guid), - string_to_wstring(endpoint.nic_mac_address))); + maybe_widen{endpoint.endpoint_guid}, + maybe_widen{endpoint.nic_mac_address})); } return fmt::format(L"{}", fmt::join(network_adapters, L", ")); @@ -293,10 +294,10 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam for (const auto& share : params.shares) { plan9_shares.push_back(fmt::format(plan9_share_template, - string_to_wstring(share.name), + maybe_widen{share.name}, share.host_path.wstring(), share.port, - string_to_wstring(share.access_name), + maybe_widen{share.access_name}, fmt::underlying(share.flags))); } @@ -364,7 +365,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam const auto vm_settings = fmt::format(vm_settings_template, params.processor_count, params.memory_size_mb, - string_to_wstring(params.name), + maybe_widen{params.name}, scsi_devices, network_adapters, plan9_shares); @@ -380,7 +381,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam return OperationResult{E_POINTER, L"HcsCreateOperation failed."}; } - const auto name_w = string_to_wstring(params.name); + const std::wstring name_w = maybe_widen{params.name}; const auto result = ResultCode{api.CreateComputeSystem(name_w.c_str(), vm_settings.c_str(), operation.get(), nullptr, &system)}; @@ -466,8 +467,8 @@ OperationResult HCSWrapper::add_endpoint(const AddEndpointParameters& params) co }})"; const auto settings = fmt::format(add_endpoint_settings_template, - string_to_wstring(params.endpoint_guid), - string_to_wstring(params.nic_mac_address)); + maybe_widen{params.endpoint_guid}, + maybe_widen{params.nic_mac_address}); return perform_hcs_operation(api, api.ModifyComputeSystem, @@ -492,7 +493,7 @@ OperationResult HCSWrapper::remove_endpoint(const std::string& compute_system_na "RequestType": "Remove" }})"; - const auto settings = fmt::format(remove_endpoint_settings_template, string_to_wstring(endpoint_guid)); + const auto settings = fmt::format(remove_endpoint_settings_template, maybe_widen{endpoint_guid}); return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } @@ -550,7 +551,7 @@ OperationResult HCSWrapper::grant_vm_access(const std::string& compute_system_na file_path.string()); const auto path_as_wstring = file_path.generic_wstring(); - const auto csname_as_wstring = string_to_wstring(compute_system_name); + const std::wstring csname_as_wstring = maybe_widen{compute_system_name}; const auto result = api.GrantVmAccess(csname_as_wstring.c_str(), path_as_wstring.c_str()); return {result, FAILED(result) ? L"GrantVmAccess failed!" : L""}; } @@ -566,7 +567,7 @@ OperationResult HCSWrapper::revoke_vm_access(const std::string& compute_system_n file_path.string()); const auto path_as_wstring = file_path.wstring(); - const auto csname_as_wstring = string_to_wstring(compute_system_name); + const std::wstring csname_as_wstring = maybe_widen{compute_system_name}; const auto result = api.RevokeVmAccess(csname_as_wstring.c_str(), path_as_wstring.c_str()); return {result, FAILED(result) ? L"RevokeVmAccess failed!" : L""}; } @@ -643,13 +644,13 @@ OperationResult HCSWrapper::add_plan9_share(const std::string& compute_system_na (void)grant_vm_access(compute_system_name, params.host_path); const auto settings = fmt::format(add_plan9_share_template, - string_to_wstring(params.name), + maybe_widen{params.name}, // generic_* always uses / as separator, use it // to normalize the path. HCS API does not like // `\` for example. preferred.generic_wstring(), params.port, - string_to_wstring(params.access_name), + maybe_widen{params.access_name}, fmt::underlying(params.flags)); fmt::print(L"{}", settings); return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); @@ -681,8 +682,8 @@ OperationResult HCSWrapper::remove_plan9_share(const std::string& compute_system }})"; const auto settings = fmt::format(remove_plan9_share_template, - string_to_wstring(params.name), - string_to_wstring(params.access_name), + maybe_widen{params.name}, + maybe_widen{params.access_name}, params.port); return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 6a60576843..b06a8f6aa6 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -31,6 +31,7 @@ #include "backends/hyperv/hyperv_virtual_machine_factory.h" #include "backends/hyperv_api/hcs_virtual_machine_factory.h" #include "backends/virtualbox/virtualbox_virtual_machine_factory.h" +#include "hyperv_api/hyperv_api_string_conversion.h" #include "logger/win_event_logger.h" #include "shared/sshfs_server_process_spec.h" #include "shared/windows/powershell.h" @@ -565,17 +566,10 @@ auto mp::platform::guid_from_wstring(const std::wstring& guid_wstr) -> ::GUID // --------------------------------------------------------- -auto mp::platform::string_to_wstring(const std::string& str) -> std::wstring -{ - return std::wstring_convert>().from_bytes(str); -} - -// --------------------------------------------------------- - auto mp::platform::guid_from_string(const std::string& guid_str) -> GUID { // Just use the wide string overload. - return guid_from_wstring(string_to_wstring(guid_str)); + return guid_from_wstring(hyperv::maybe_widen{guid_str}); } // --------------------------------------------------------- @@ -844,7 +838,7 @@ std::map mp::platform::Platform::get_netw { if (netinfo.links.empty()) { - const std::wstring search = fmt::format(L"vEthernet ({})", string_to_wstring(netinfo.id)); + const std::wstring search = fmt::format(L"vEthernet ({})", hyperv::maybe_widen{netinfo.id}); for (auto pitr = adapter_info; pitr; pitr = pitr->Next) { const auto& adapter = *pitr; From 9d185fd2d338cec0475e2d6fc36acde008f1970b Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:26:15 +0300 Subject: [PATCH 49/76] [hyperv-api] remove redundant hyperv_api_common{.cpp/.h} --- .../backends/hyperv_api/CMakeLists.txt | 1 - .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 12 +- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 6 +- .../hcs_virtual_machine_factory.cpp | 1 - .../backends/hyperv_api/hyperv_api_common.cpp | 139 ------------------ .../backends/hyperv_api/hyperv_api_common.h | 82 ----------- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 22 +-- 7 files changed, 20 insertions(+), 243 deletions(-) delete mode 100644 src/platform/backends/hyperv_api/hyperv_api_common.cpp delete mode 100644 src/platform/backends/hyperv_api/hyperv_api_common.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index e29a7cd94f..97adcf41e0 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -41,7 +41,6 @@ if(WIN32) endif() add_library(hyperv_api_backend STATIC - hyperv_api_common.cpp hcn/hyperv_hcn_api_wrapper.cpp hcn/hyperv_hcn_route.cpp hcn/hyperv_hcn_subnet.cpp diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 9fb1872906..0f585a96b0 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -20,9 +20,9 @@ #include #include #include -#include #include +#include #include // clang-format off @@ -125,7 +125,7 @@ UniqueHcnNetwork open_network(const HCNAPITable& api, const std::string& network mpl::debug(kLogCategory, "open_network(...) > network_guid: {} ", network_guid); HCN_NETWORK network{nullptr}; - const auto result = perform_hcn_operation(api, api.OpenNetwork, guid_from_string(network_guid), &network); + const auto result = perform_hcn_operation(api, api.OpenNetwork, platform::guid_from_string(network_guid), &network); if (!result) { mpl::error(kLogCategory, "open_network() > HcnOpenNetwork failed with {}!", result.code); @@ -202,7 +202,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params HCN_NETWORK network{nullptr}; const auto result = perform_hcn_operation(api, api.CreateNetwork, - guid_from_string(params.guid), + platform::guid_from_string(params.guid), network_settings.c_str(), &network); @@ -224,7 +224,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params OperationResult HCNWrapper::delete_network(const std::string& network_guid) const { mpl::debug(kLogCategory, "HCNWrapper::delete_network(...) > network_guid: {}", network_guid); - return perform_hcn_operation(api, api.DeleteNetwork, guid_from_string(network_guid)); + return perform_hcn_operation(api, api.DeleteNetwork, platform::guid_from_string(network_guid)); } // --------------------------------------------------------- @@ -264,7 +264,7 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para const auto result = perform_hcn_operation(api, api.CreateEndpoint, network.get(), - guid_from_string(params.endpoint_guid), + platform::guid_from_string(params.endpoint_guid), endpoint_settings.c_str(), &endpoint); [[maybe_unused]] UniqueHcnEndpoint _{endpoint, api.CloseEndpoint}; @@ -276,7 +276,7 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para OperationResult HCNWrapper::delete_endpoint(const std::string& endpoint_guid) const { mpl::debug(kLogCategory, "HCNWrapper::delete_endpoint(...) > endpoint_guid: {} ", endpoint_guid); - return perform_hcn_operation(api, api.DeleteEndpoint, guid_from_string(endpoint_guid)); + return perform_hcn_operation(api, api.DeleteEndpoint, platform::guid_from_string(endpoint_guid)); } } // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 06123e3e05..09ffdddd31 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -14,13 +14,13 @@ * along with this program. If not, see . * */ +#include -#include "hyperv_api/hyperv_api_string_conversion.h" #include #include -#include #include -#include +#include + #include #include diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp index 862b606fc9..cfcbf800c4 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include diff --git a/src/platform/backends/hyperv_api/hyperv_api_common.cpp b/src/platform/backends/hyperv_api/hyperv_api_common.cpp deleted file mode 100644 index 133745ea59..0000000000 --- a/src/platform/backends/hyperv_api/hyperv_api_common.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include -#include - -#include - -#include // for CLSIDFromString - -#include -#include - -/** - * Formatter for GUID type - */ -template -struct fmt::formatter<::GUID, Char> -{ - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - - template - auto format(const ::GUID& guid, FormatContext& ctx) const - { - // The format string is laid out char by char to allow it - // to be used for initializing variables with different character - // sizes. - static constexpr Char guid_f[] = {'{', ':', '0', '8', 'x', '}', '-', '{', ':', '0', '4', 'x', '}', '-', '{', - ':', '0', '4', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', - '2', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', - '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', - '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', 0}; - return format_to(ctx.out(), - guid_f, - guid.Data1, - guid.Data2, - guid.Data3, - guid.Data4[0], - guid.Data4[1], - guid.Data4[2], - guid.Data4[3], - guid.Data4[4], - guid.Data4[5], - guid.Data4[6], - guid.Data4[7]); - } -}; - -namespace multipass::hyperv -{ - -struct GuidParseError : FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -auto guid_from_wstring(const std::wstring& guid_wstr) -> ::GUID -{ - constexpr auto kGUIDLength = 36; - constexpr auto kGUIDLengthWithBraces = kGUIDLength + 2; - - const auto input = [&guid_wstr]() { - switch (guid_wstr.length()) - { - case kGUIDLength: - // CLSIDFromString requires GUIDs to be wrapped with braces. - return fmt::format(L"{{{}}}", guid_wstr); - case kGUIDLengthWithBraces: - { - if (*guid_wstr.begin() != L'{' || *std::prev(guid_wstr.end()) != L'}') - { - throw GuidParseError{"GUID string either does not start or end with a brace."}; - } - return guid_wstr; - } - } - throw GuidParseError{"Invalid length for a GUID string ({}).", guid_wstr.length()}; - }(); - - ::GUID guid = {}; - - const auto result = CLSIDFromString(input.c_str(), &guid); - - if (FAILED(result)) - { - throw GuidParseError{"Failed to parse the GUID string ({}).", result}; - } - - return guid; -} - -// --------------------------------------------------------- - -auto string_to_wstring(const std::string& str) -> std::wstring -{ - return std::wstring_convert>().from_bytes(str); -} - -// --------------------------------------------------------- - -auto guid_from_string(const std::string& guid_str) -> GUID -{ - // Just use the wide string overload. - return guid_from_wstring(string_to_wstring(guid_str)); -} - -// --------------------------------------------------------- - -auto guid_to_string(const ::GUID& guid) -> std::string -{ - - return fmt::format("{}", guid); -} - -// --------------------------------------------------------- - -auto guid_to_wstring(const ::GUID& guid) -> std::wstring -{ - return fmt::format(L"{}", guid); -} - -} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_common.h b/src/platform/backends/hyperv_api/hyperv_api_common.h deleted file mode 100644 index 8bc0b021f8..0000000000 --- a/src/platform/backends/hyperv_api/hyperv_api_common.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 . - * - */ - -#ifndef MULTIPASS_HYPERV_API_COMMON_H -#define MULTIPASS_HYPERV_API_COMMON_H - -#include - -#include - -namespace multipass::hyperv -{ - -// --------------------------------------------------------- - -/** - * Parse given GUID string into a GUID struct. - * - * @param guid_str GUID in string form, either 36 characters - * (without braces) or 38 characters (with braces.) - * - * @return GUID The parsed GUID - */ -[[nodiscard]] auto guid_from_string(const std::string& guid_str) -> GUID; - -/** - * Parse given GUID string into a GUID struct. - * - * @param guid_wstr GUID in string form, either 36 characters - * (without braces) or 38 characters (with braces.) - * - * @return GUID The parsed GUID - */ -[[nodiscard]] auto guid_from_wstring(const std::wstring& guid_wstr) -> GUID; - -// --------------------------------------------------------- - -/** - * @brief Convert a GUID to its string representation - * - * @param [in] guid GUID to convert - * @return std::string GUID in string form - */ -[[nodiscard]] auto guid_to_string(const ::GUID& guid) -> std::string; - -// --------------------------------------------------------- - -/** - * @brief Convert a guid to its wide string representation - * - * @param [in] guid GUID to convert - * @return std::wstring GUID in wstring form - */ -[[nodiscard]] auto guid_to_wstring(const ::GUID& guid) -> std::wstring; - -// --------------------------------------------------------- - -/** - * Convert a multi-byte string to a wide-character string. - * - * @param str Multi-byte string - * @return Wide-character equivalent of the given multi-byte string. - */ -[[nodiscard]] auto string_to_wstring(const std::string& str) -> std::wstring; - -} // namespace multipass::hyperv - -#endif // MULTIPASS_HYPERV_API_COMMON_H diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index c36f14ddf1..e80415ad48 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -23,9 +23,9 @@ #include #include #include -#include #include +#include #include #include @@ -165,7 +165,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_ics) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = hyperv::guid_to_string(id); + const auto guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -244,7 +244,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_transparent) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = hyperv::guid_to_string(id); + const auto guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -334,7 +334,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_with_flags_multiple_polici const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = hyperv::guid_to_string(id); + const auto guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -432,7 +432,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_multiple_ipams) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = hyperv::guid_to_string(id); + const auto guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -602,7 +602,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_network_success) EXPECT_CALL(mock_delete_network, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = hyperv::guid_to_string(guid); + const auto guid_str = platform::guid_to_string(guid); ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); @@ -728,7 +728,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto endpoint_guid_str = hyperv::guid_to_string(id); + const auto endpoint_guid_str = platform::guid_to_string(id); ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", endpoint_guid_str); *endpoint = mock_endpoint_object; }, @@ -740,7 +740,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = hyperv::guid_to_string(id); + const auto expected_network_guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); ASSERT_NE(nullptr, network); ASSERT_EQ(nullptr, *network); @@ -875,7 +875,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto expected_endpoint_guid_str = hyperv::guid_to_string(id); + const auto expected_endpoint_guid_str = platform::guid_to_string(id); ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", expected_endpoint_guid_str); *endpoint = mock_endpoint_object; *error_record = mock_error_msg; @@ -888,7 +888,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = hyperv::guid_to_string(id); + const auto expected_network_guid_str = platform::guid_to_string(id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); ASSERT_NE(nullptr, error_record); ASSERT_EQ(nullptr, *error_record); @@ -950,7 +950,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_success) EXPECT_CALL(mock_delete_endpoint, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = hyperv::guid_to_string(guid); + const auto guid_str = platform::guid_to_string(guid); ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); From 3d3524237ef815bc940d72fc3e6d3dc42bc9dc46 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:26:50 +0300 Subject: [PATCH 50/76] [platform_win] remove commented out get_network_interfaces_info fn --- src/platform/platform_win.cpp | 36 ----------------------------------- 1 file changed, 36 deletions(-) diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index b06a8f6aa6..f459827cb7 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -587,42 +587,6 @@ auto mp::platform::guid_to_wstring(const ::GUID& guid) -> std::wstring return fmt::format(L"{}", guid); } -// std::map mp::platform::Platform::get_network_interfaces_info() const -// { -// static const auto ps_cmd_base = QStringLiteral( -// "Get-NetAdapter -physical | Select-Object -Property Name,MediaType,PhysicalMediaType,InterfaceDescription"); -// static const auto ps_args = QString{ps_cmd_base}.split(' ', Qt::SkipEmptyParts) + -// PowerShell::Snippets::to_bare_csv; - -// QString ps_output; -// QString ps_output_err; -// if (PowerShell::exec(ps_args, "Network Listing on Windows Platform", &ps_output, &ps_output_err)) -// { -// std::map ret{}; -// for (const auto& line : ps_output.split(QRegularExpression{"[\r\n]"}, Qt::SkipEmptyParts)) -// { -// auto terms = line.split(',', Qt::KeepEmptyParts); -// if (terms.size() != 4) -// { -// throw std::runtime_error{ -// fmt::format("Could not determine available networks - unexpected powershell output: {}", -// ps_output)}; -// } - -// auto iface = mp::NetworkInterfaceInfo{terms[0].toStdString(), -// interpret_net_type(terms[1], terms[2]), -// terms[3].toStdString()}; -// ret.emplace(iface.id, iface); -// } - -// return ret; -// } - -// auto detail = ps_output_err.isEmpty() ? "" : fmt::format(" Detail: {}", ps_output_err); -// auto err = fmt::format("Could not determine available networks - error executing powershell command.{}", detail); -// throw std::runtime_error{err}; -// } - mp::platform::wsa_init_wrapper::wsa_init_wrapper() : wsa_data(new ::WSAData()), wsa_init_result(::WSAStartup(MAKEWORD(2, 2), wsa_data)) { From 215cd4582a89e821c8e68960411b1caffa1a4910 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:35:03 +0300 Subject: [PATCH 51/76] [hyperv-hcn] move guid_from_string to HCN wrapper --- include/multipass/platform_win.h | 21 ----- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 76 +++++++++++++++++-- src/platform/platform_win.cpp | 48 ------------ 3 files changed, 70 insertions(+), 75 deletions(-) diff --git a/include/multipass/platform_win.h b/include/multipass/platform_win.h index c80239a752..0c89055118 100644 --- a/include/multipass/platform_win.h +++ b/include/multipass/platform_win.h @@ -47,27 +47,6 @@ struct wsa_init_wrapper const int wsa_init_result{-1}; }; -// --------------------------------------------------------- - -/** - * Parse given GUID string into a GUID struct. - * - * @param guid_str GUID in string form, either 36 characters - * (without braces) or 38 characters (with braces.) - * - * @return GUID The parsed GUID - */ -[[nodiscard]] auto guid_from_string(const std::string& guid_str) -> GUID; - -/** - * Parse given GUID string into a GUID struct. - * - * @param guid_wstr GUID in string form, either 36 characters - * (without braces) or 38 characters (with braces.) - * - * @return GUID The parsed GUID - */ -[[nodiscard]] auto guid_from_wstring(const std::wstring& guid_wstr) -> GUID; // --------------------------------------------------------- diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 0f585a96b0..de97380d79 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -21,8 +21,8 @@ #include #include +#include #include -#include #include // clang-format off @@ -43,6 +43,11 @@ namespace multipass::hyperv::hcn { +struct GuidParseError : multipass::FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + namespace { @@ -62,6 +67,65 @@ constexpr auto kLogCategory = "HyperV-HCN-Wrapper"; // --------------------------------------------------------- +/** + * Parse given GUID string into a GUID struct. + * + * @param guid_str GUID in string form, either 36 characters + * (without braces) or 38 characters (with braces.) + * + * @return GUID The parsed GUID + */ +auto guid_from_string(const std::wstring& guid_wstr) -> ::GUID +{ + constexpr auto kGUIDLength = 36; + constexpr auto kGUIDLengthWithBraces = kGUIDLength + 2; + + const auto input = [&guid_wstr]() { + switch (guid_wstr.length()) + { + case kGUIDLength: + // CLSIDFromString requires GUIDs to be wrapped with braces. + return fmt::format(L"{{{}}}", guid_wstr); + case kGUIDLengthWithBraces: + { + if (*guid_wstr.begin() != L'{' || *std::prev(guid_wstr.end()) != L'}') + { + throw GuidParseError{"GUID string either does not start or end with a brace."}; + } + return guid_wstr; + } + } + throw GuidParseError{"Invalid length for a GUID string ({}).", guid_wstr.length()}; + }(); + + ::GUID guid = {}; + + const auto result = CLSIDFromString(input.c_str(), &guid); + + if (FAILED(result)) + { + throw GuidParseError{"Failed to parse the GUID string ({}).", result}; + } + + return guid; +} + +/** + * Parse given GUID string into a GUID struct. + * + * @param guid_str GUID in string form, either 36 characters + * (without braces) or 38 characters (with braces.) + * + * @return GUID The parsed GUID + */ +auto guid_from_string(const std::string& guid_wstr) -> ::GUID +{ + const std::wstring v = maybe_widen{guid_wstr}; + return guid_from_string(v); +} + +// --------------------------------------------------------- + /** * Perform a Host Compute Network API operation * @@ -125,7 +189,7 @@ UniqueHcnNetwork open_network(const HCNAPITable& api, const std::string& network mpl::debug(kLogCategory, "open_network(...) > network_guid: {} ", network_guid); HCN_NETWORK network{nullptr}; - const auto result = perform_hcn_operation(api, api.OpenNetwork, platform::guid_from_string(network_guid), &network); + const auto result = perform_hcn_operation(api, api.OpenNetwork, guid_from_string(network_guid), &network); if (!result) { mpl::error(kLogCategory, "open_network() > HcnOpenNetwork failed with {}!", result.code); @@ -202,7 +266,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params HCN_NETWORK network{nullptr}; const auto result = perform_hcn_operation(api, api.CreateNetwork, - platform::guid_from_string(params.guid), + guid_from_string(params.guid), network_settings.c_str(), &network); @@ -224,7 +288,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params OperationResult HCNWrapper::delete_network(const std::string& network_guid) const { mpl::debug(kLogCategory, "HCNWrapper::delete_network(...) > network_guid: {}", network_guid); - return perform_hcn_operation(api, api.DeleteNetwork, platform::guid_from_string(network_guid)); + return perform_hcn_operation(api, api.DeleteNetwork, guid_from_string(network_guid)); } // --------------------------------------------------------- @@ -264,7 +328,7 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para const auto result = perform_hcn_operation(api, api.CreateEndpoint, network.get(), - platform::guid_from_string(params.endpoint_guid), + guid_from_string(params.endpoint_guid), endpoint_settings.c_str(), &endpoint); [[maybe_unused]] UniqueHcnEndpoint _{endpoint, api.CloseEndpoint}; @@ -276,7 +340,7 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para OperationResult HCNWrapper::delete_endpoint(const std::string& endpoint_guid) const { mpl::debug(kLogCategory, "HCNWrapper::delete_endpoint(...) > endpoint_guid: {} ", endpoint_guid); - return perform_hcn_operation(api, api.DeleteEndpoint, platform::guid_from_string(endpoint_guid)); + return perform_hcn_operation(api, api.DeleteEndpoint, guid_from_string(endpoint_guid)); } } // namespace multipass::hyperv::hcn diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index f459827cb7..2a5aabd1a7 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -524,54 +524,6 @@ struct fmt::formatter<::GUID, Char> } }; -struct GuidParseError : multipass::FormattedExceptionBase<> -{ - using FormattedExceptionBase<>::FormattedExceptionBase; -}; - -auto mp::platform::guid_from_wstring(const std::wstring& guid_wstr) -> ::GUID -{ - constexpr auto kGUIDLength = 36; - constexpr auto kGUIDLengthWithBraces = kGUIDLength + 2; - - const auto input = [&guid_wstr]() { - switch (guid_wstr.length()) - { - case kGUIDLength: - // CLSIDFromString requires GUIDs to be wrapped with braces. - return fmt::format(L"{{{}}}", guid_wstr); - case kGUIDLengthWithBraces: - { - if (*guid_wstr.begin() != L'{' || *std::prev(guid_wstr.end()) != L'}') - { - throw GuidParseError{"GUID string either does not start or end with a brace."}; - } - return guid_wstr; - } - } - throw GuidParseError{"Invalid length for a GUID string ({}).", guid_wstr.length()}; - }(); - - ::GUID guid = {}; - - const auto result = CLSIDFromString(input.c_str(), &guid); - - if (FAILED(result)) - { - throw GuidParseError{"Failed to parse the GUID string ({}).", result}; - } - - return guid; -} - -// --------------------------------------------------------- - -auto mp::platform::guid_from_string(const std::string& guid_str) -> GUID -{ - // Just use the wide string overload. - return guid_from_wstring(hyperv::maybe_widen{guid_str}); -} - // --------------------------------------------------------- auto mp::platform::guid_to_string(const ::GUID& guid) -> std::string From 67fd63a6702f191e7a2ead62175b57b9f0677d3e Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:42:49 +0300 Subject: [PATCH 52/76] [platform/win] remove guid_to_string functions - move GUID formatter to the platform_win.h header - update unit test usages --- include/multipass/platform_win.h | 57 +++++++++++++-------- src/platform/platform_win.cpp | 53 ------------------- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 20 ++++---- 3 files changed, 47 insertions(+), 83 deletions(-) diff --git a/include/multipass/platform_win.h b/include/multipass/platform_win.h index 0c89055118..53384011f9 100644 --- a/include/multipass/platform_win.h +++ b/include/multipass/platform_win.h @@ -20,6 +20,8 @@ #include +#include + #include struct WSAData; @@ -47,29 +49,44 @@ struct wsa_init_wrapper const int wsa_init_result{-1}; }; - -// --------------------------------------------------------- - -/** - * @brief Convert a GUID to its string representation - * - * @param [in] guid GUID to convert - * @return std::string GUID in string form - */ -[[nodiscard]] auto guid_to_string(const ::GUID& guid) -> std::string; - -// --------------------------------------------------------- +} // namespace multipass::platform /** - * @brief Convert a guid to its wide string representation - * - * @param [in] guid GUID to convert - * @return std::wstring GUID in wstring form + * Formatter for GUID type */ -[[nodiscard]] auto guid_to_wstring(const ::GUID& guid) -> std::wstring; - -// --------------------------------------------------------- +template +struct fmt::formatter<::GUID, Char> +{ + constexpr auto parse(basic_format_parse_context& ctx) + { + return ctx.begin(); + } -} // namespace multipass::platform + template + auto format(const ::GUID& guid, FormatContext& ctx) const + { + // The format string is laid out char by char to allow it + // to be used for initializing variables with different character + // sizes. + static constexpr Char guid_f[] = {'{', ':', '0', '8', 'x', '}', '-', '{', ':', '0', '4', 'x', '}', '-', '{', + ':', '0', '4', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', + '2', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', + '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', + '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', 0}; + return format_to(ctx.out(), + guid_f, + guid.Data1, + guid.Data2, + guid.Data3, + guid.Data4[0], + guid.Data4[1], + guid.Data4[2], + guid.Data4[3], + guid.Data4[4], + guid.Data4[5], + guid.Data4[6], + guid.Data4[7]); + } +}; #endif // MULTIPASS_PLATFORM_WIN_H diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 2a5aabd1a7..a1468ea0c2 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -486,59 +486,6 @@ std::filesystem::path multipass_final_storage_location() } } // namespace -/** - * Formatter for GUID type - */ -template -struct fmt::formatter<::GUID, Char> -{ - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - - template - auto format(const ::GUID& guid, FormatContext& ctx) const - { - // The format string is laid out char by char to allow it - // to be used for initializing variables with different character - // sizes. - static constexpr Char guid_f[] = {'{', ':', '0', '8', 'x', '}', '-', '{', ':', '0', '4', 'x', '}', '-', '{', - ':', '0', '4', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', - '2', 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', - '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', - '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', 0}; - return format_to(ctx.out(), - guid_f, - guid.Data1, - guid.Data2, - guid.Data3, - guid.Data4[0], - guid.Data4[1], - guid.Data4[2], - guid.Data4[3], - guid.Data4[4], - guid.Data4[5], - guid.Data4[6], - guid.Data4[7]); - } -}; - -// --------------------------------------------------------- - -auto mp::platform::guid_to_string(const ::GUID& guid) -> std::string -{ - - return fmt::format("{}", guid); -} - -// --------------------------------------------------------- - -auto mp::platform::guid_to_wstring(const ::GUID& guid) -> std::wstring -{ - return fmt::format(L"{}", guid); -} - mp::platform::wsa_init_wrapper::wsa_init_wrapper() : wsa_data(new ::WSAData()), wsa_init_result(::WSAStartup(MAKEWORD(2, 2), wsa_data)) { diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index e80415ad48..1d299c37ed 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -165,7 +165,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_ics) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = platform::guid_to_string(id); + const auto guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -244,7 +244,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_transparent) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = platform::guid_to_string(id); + const auto guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -334,7 +334,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_with_flags_multiple_polici const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = platform::guid_to_string(id); + const auto guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -432,7 +432,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_multiple_ipams) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = platform::guid_to_string(id); + const auto guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); *network = mock_network_object; }, @@ -602,7 +602,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_network_success) EXPECT_CALL(mock_delete_network, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = platform::guid_to_string(guid); + const auto guid_str = fmt::format("{}", guid); ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); @@ -728,7 +728,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto endpoint_guid_str = platform::guid_to_string(id); + const auto endpoint_guid_str = fmt::format("{}", id); ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", endpoint_guid_str); *endpoint = mock_endpoint_object; }, @@ -740,7 +740,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = platform::guid_to_string(id); + const auto expected_network_guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); ASSERT_NE(nullptr, network); ASSERT_EQ(nullptr, *network); @@ -875,7 +875,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto expected_endpoint_guid_str = platform::guid_to_string(id); + const auto expected_endpoint_guid_str = fmt::format("{}", id); ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", expected_endpoint_guid_str); *endpoint = mock_endpoint_object; *error_record = mock_error_msg; @@ -888,7 +888,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = platform::guid_to_string(id); + const auto expected_network_guid_str = fmt::format("{}", id); ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); ASSERT_NE(nullptr, error_record); ASSERT_EQ(nullptr, *error_record); @@ -950,7 +950,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_success) EXPECT_CALL(mock_delete_endpoint, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = platform::guid_to_string(guid); + const auto guid_str = fmt::format("{}", guid); ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); From d25da6ddc56165904c1c63a991d632dca94fa0cd Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 16:49:33 +0300 Subject: [PATCH 53/76] [platform/win] comment ip_utils --- src/platform/platform_win.cpp | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index a1468ea0c2..07a43a33f4 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -523,11 +523,27 @@ struct InvalidNetworkPrefixLengthException : public multipass::FormattedExceptio using multipass::FormattedExceptionBase<>::FormattedExceptionBase; }; +/** + * IP conversion utilities + */ static const auto& ip_utils() { + // Winsock initialization has to happen before we can call network + // related functions, even the conversion ones (e.g. inet_ntop) static multipass::platform::wsa_init_wrapper wrapper; + + /** + * Helper struct that provides address conversion + * utilities + */ struct ip_utils { + /** + * Convert IPv4 address to string + * + * @param [in] addr IPv4 address as uint32 + * @return std::string String representation of @p addr + */ static std::string to_string(std::uint32_t addr) { char str[INET_ADDRSTRLEN] = {}; @@ -536,6 +552,12 @@ static const auto& ip_utils() return str; } + /** + * Convert IPv6 address to string + * + * @param [in] addr IPv6 address + * @return std::string String representation of @p addr + */ static std::string to_string(const in6_addr& addr) { char str[INET6_ADDRSTRLEN] = {}; @@ -544,6 +566,13 @@ static const auto& ip_utils() return str; } + /** + * Convert an IPv4 address to network CIDR + * + * @param [in] v4 IPv4 address + * @param [in] prefix_length Network prefix + * @return std::string Network address in CIDR form + */ static auto to_network(const in_addr& v4, std::uint8_t prefix_length) { // Convert to the host long first so we can apply a mask to it @@ -560,6 +589,13 @@ static const auto& ip_utils() return fmt::format("{}/{}", to_string(network_hbo), prefix_length); } + /** + * Convert an IPv6 address to network CIDR + * + * @param [in] v6 IPv6 address + * @param [in] prefix_length Network prefix + * @return std::string Network address in CIDR form + */ static auto to_network(const in6_addr& v6, std::uint8_t prefix_length) { // Convert to the host long first so we can apply a mask to it @@ -582,6 +618,8 @@ static const auto& ip_utils() return fmt::format("{}/{}", network_addr, prefix_length); } } static helper; + + // Initialize once and reuse. return helper; } @@ -718,7 +756,6 @@ std::map mp::platform::Platform::get_netw } else { - // TODO: FormatMessage. throw GetNetworkInterfacesInfoException{"Failed to retrieve network interface information. Error code: {}", result}; } From 145315317768b750faa171917dc68b53ecb349ac Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 17:35:21 +0300 Subject: [PATCH 54/76] [hyperv-hcs] extract SCSI device to its own entity - Introduce two new types, HcsScsiDevice & HcsScsiDeviceType - Provide proper formatting for the HcsScsiDevice - Remove vhdx_path & cloudinit_iso_path variables from create params - Introduce scsi_devices paraneter to create params - Update the code that refers to the vhdx_path & cloudinit_iso_path --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 38 +---------- .../hyperv_hcs_create_compute_system_params.h | 17 ++--- .../hyperv_api/hcs/hyperv_hcs_scsi_device.cpp | 58 ++++++++++++++++ .../hyperv_api/hcs/hyperv_hcs_scsi_device.h | 52 ++++++++++++++ .../hcs/hyperv_hcs_scsi_device_type.h | 63 +++++++++++++++++ .../hyperv_api/hcs_virtual_machine.cpp | 19 +++++- tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 20 +++--- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 68 ++++++++++++------- 9 files changed, 248 insertions(+), 88 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.cpp create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device_type.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 97adcf41e0..bf7a9a670f 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -47,6 +47,7 @@ if(WIN32) hcn/hyperv_hcn_ipam.cpp hcn/hyperv_hcn_network_policy.cpp hcs/hyperv_hcs_api_wrapper.cpp + hcs/hyperv_hcs_scsi_device.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 09ffdddd31..8cfc9f3532 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -223,42 +223,6 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - // Fill the SCSI devices template depending on - // available drives. - const auto scsi_devices = [¶ms]() { - constexpr auto scsi_device_template = LR"( - "{0}": {{ - "Attachments": {{ - "0": {{ - "Type": "{1}", - "Path": "{2}", - "ReadOnly": {3} - }} - }} - }})"; - std::vector scsi_nodes{}; - - if (!params.cloudinit_iso_path.empty()) - { - scsi_nodes.push_back(fmt::format(scsi_device_template, - L"cloud-init iso file", - L"Iso", - params.cloudinit_iso_path.generic_wstring(), - true)); - } - - if (!params.vhdx_path.empty()) - { - scsi_nodes.push_back(fmt::format(scsi_device_template, - L"Primary disk", - L"VirtualDisk", - params.vhdx_path.generic_wstring(), - false)); - } - - return fmt::format(L"{}", fmt::join(scsi_nodes, L", ")); - }(); - const auto network_adapters = [&]() { std::vector network_adapters = {}; @@ -366,7 +330,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam params.processor_count, params.memory_size_mb, maybe_widen{params.name}, - scsi_devices, + fmt::join(params.scsi_devices, L","), network_adapters, plan9_shares); diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 41f5ab1c84..e74ad8c124 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -49,14 +50,9 @@ struct CreateComputeSystemParameters std::uint32_t processor_count{}; /** - * Path to the cloud-init ISO file + * List of SCSI devices that are attached on boot */ - std::filesystem::path cloudinit_iso_path{}; - - /** - * Path to the Primary (boot) VHDX file - */ - std::filesystem::path vhdx_path{}; + std::vector scsi_devices; /** * List of endpoints that'll be added to the compute system @@ -88,13 +84,10 @@ struct fmt::formatter. + * + */ +#include + +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsScsiDevice; +using multipass::hyperv::hcs::HcsScsiDeviceType; + +template +template +auto fmt::formatter::format(const HcsScsiDevice& scsi_device, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto scsi_device_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + "{0}": {{ + "Attachments": {{ + "0": {{ + "Type": "{1}", + "Path": "{2}", + "ReadOnly": {3} + }} + }} + }} + )json"); + + return format_to(ctx.out(), + scsi_device_template.as(), + maybe_widen{scsi_device.name}, + maybe_widen{scsi_device.type}, + scsi_device.path, + scsi_device.read_only); +} + +template auto fmt::formatter::format(const HcsScsiDevice&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const HcsScsiDevice&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h new file mode 100644 index 0000000000..ff0d6dd074 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h @@ -0,0 +1,52 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H +#define MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H + +#include + +#include + +#include +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsScsiDevice +{ + HcsScsiDeviceType type; + std::string name; + std::filesystem::path path; + bool read_only; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for HcnNetworkPolicy + */ +template +struct fmt::formatter : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsScsiDevice& policy, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +#endif // MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device_type.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device_type.h new file mode 100644 index 0000000000..57ddcdb203 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device_type.h @@ -0,0 +1,63 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_TYPE_H +#define MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_TYPE_H + +#include +#include + +namespace multipass::hyperv::hcs +{ + +/** + * Strongly-typed string values for + * SCSI device type. + */ +struct HcsScsiDeviceType +{ + operator std::string_view() const + { + return value; + } + + operator std::string() const + { + return std::string{value}; + } + + constexpr static auto Iso() + { + return HcsScsiDeviceType{"Iso"}; + } + + constexpr static auto VirtualDisk() + { + return HcsScsiDeviceType{"VirtualDisk"}; + } + +private: + constexpr HcsScsiDeviceType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcs + +#endif // MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_TYPE_H diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 4eef1cc32b..b4ea6822f4 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -297,8 +297,18 @@ bool HCSVirtualMachine::maybe_create_compute_system() params.name = description.vm_name; params.memory_size_mb = description.mem_size.in_megabytes(); params.processor_count = description.num_cores; - params.cloudinit_iso_path = description.cloud_init_iso.toStdString(); - params.vhdx_path = get_primary_disk_path(); + + hcs::HcsScsiDevice primary_disk{hcs::HcsScsiDeviceType::VirtualDisk()}; + primary_disk.name = "Primary disk"; + primary_disk.path = get_primary_disk_path(); + primary_disk.read_only = false; + params.scsi_devices.push_back(primary_disk); + + hcs::HcsScsiDevice cloudinit_iso{hcs::HcsScsiDeviceType::Iso()}; + cloudinit_iso.name = "cloud-init ISO file"; + cloudinit_iso.path = description.cloud_init_iso.toStdString(); + cloudinit_iso.read_only = true; + params.scsi_devices.push_back(cloudinit_iso); const auto create_to_add = [this](const auto& create_params) { hcs::AddEndpointParameters add_params{}; @@ -326,7 +336,10 @@ bool HCSVirtualMachine::maybe_create_compute_system() } // Grant access to the VHDX and the cloud-init ISO files. - grant_access_to_paths({create_compute_system_params.cloudinit_iso_path, create_compute_system_params.vhdx_path}); + for(const auto & scsi : create_compute_system_params.scsi_devices){ + grant_access_to_paths({scsi.path}); + } + return true; } diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index a2176cf3cd..9c0945c778 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -46,8 +46,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) params.name = "test"; params.memory_size_mb = 1024; params.processor_count = 1; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); const auto c_result = uut.create_compute_system(params); ASSERT_TRUE(uut.get_compute_system_state(params.name, state)); @@ -71,8 +71,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, pause_resume_compute_system) params.name = "test"; params.memory_size_mb = 1024; params.processor_count = 1; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; ASSERT_TRUE(uut.create_compute_system(params)); @@ -104,8 +104,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, enumerate_properties) params.name = "test"; params.memory_size_mb = 1024; params.processor_count = 1; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); const auto c_result = uut.create_compute_system(params); @@ -139,8 +139,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) params.name = "test"; params.memory_size_mb = 1024; params.processor_count = 1; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); const auto c_result = uut.create_compute_system(params); @@ -180,8 +180,8 @@ TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) params.name = "test"; params.memory_size_mb = 1024; params.processor_count = 1; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); const auto c_result = uut.create_compute_system(params); diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 832afbc5fc..4eac4d2b36 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -219,7 +219,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -228,7 +228,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -311,7 +311,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB) | cloud-init ISO path: (cloudinit iso path) | VHDX path: (virtual disk path)"); + "Memory size: (16384 MiB)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -324,8 +324,12 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = "cloudinit iso path"; - params.vhdx_path = "virtual disk path"; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), + "cloud-init", + "cloudinit iso path", + true}); + params.scsi_devices.push_back( + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}); params.memory_size_mb = 16384; params.processor_count = 8; @@ -395,7 +399,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) } }, "Scsi": { - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -478,7 +482,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB) | cloud-init ISO path: () | VHDX path: (virtual disk path)"); + "Memory size: (16384 MiB)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -491,8 +495,8 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = ""; - params.vhdx_path = "virtual disk path"; + params.scsi_devices.push_back( + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}); params.memory_size_mb = 16384; params.processor_count = 8; @@ -562,7 +566,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -645,7 +649,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB) | cloud-init ISO path: (cloudinit iso path) | VHDX path: ()"); + "Memory size: (16384 MiB)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -658,8 +662,10 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = "cloudinit iso path"; - params.vhdx_path = ""; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), + "cloud-init", + "cloudinit iso path", + true}); params.memory_size_mb = 16384; params.processor_count = 8; @@ -802,7 +808,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) logger_scope.mock_logger->expect_log( mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB) | cloud-init ISO path: () | VHDX path: ()"); + "Memory size: (16384 MiB)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -815,8 +821,6 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = ""; - params.vhdx_path = ""; params.memory_size_mb = 16384; params.processor_count = 8; @@ -866,8 +870,12 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_create_operation_fail) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = "cloudinit iso path"; - params.vhdx_path = "virtual disk path"; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), + "cloud-init", + "cloudinit iso path", + true}); + params.scsi_devices.push_back( + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}); params.memory_size_mb = 16384; params.processor_count = 8; @@ -931,7 +939,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -940,7 +948,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -1012,8 +1020,12 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = "cloudinit iso path"; - params.vhdx_path = "virtual disk path"; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), + "cloud-init", + "cloudinit iso path", + true}); + params.scsi_devices.push_back( + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}); params.memory_size_mb = 16384; params.processor_count = 8; @@ -1083,7 +1095,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -1092,7 +1104,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -1185,8 +1197,12 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) uut_t uut{mock_api_table}; hyperv::hcs::CreateComputeSystemParameters params{}; params.name = "test_vm"; - params.cloudinit_iso_path = "cloudinit iso path"; - params.vhdx_path = "virtual disk path"; + params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), + "cloud-init", + "cloudinit iso path", + true}); + params.scsi_devices.push_back( + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}); params.memory_size_mb = 16384; params.processor_count = 8; From ba4d72f7d99c0d8bf01abba1eb8d047a4949b151 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 17:38:13 +0300 Subject: [PATCH 55/76] [hyperv-api] make the linter happy --- include/multipass/virtual_machine.h | 2 +- .../backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 28a4eccb65..5dbb36261c 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -28,12 +28,12 @@ #include #include +#include #include #include #include #include #include -#include #include diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp index 13334bb832..12eaf3f998 100644 --- a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api_wrapper.cpp @@ -26,8 +26,8 @@ #include #include -#include #include +#include namespace multipass::hyperv::virtdisk { From 9709c3ae45897aa54551eca61971516a83b356e3 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 18:28:23 +0300 Subject: [PATCH 56/76] [hyperv-hcs] extract network adapter to its own entity - replace "endpoints" with "network_adapters" in create params - Introduce new HcsNetworkAdapter type - Implement proper JSON formatter for HcsNetworkAdapter - Update relevant references in code --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 22 +-------- .../hyperv_hcs_create_compute_system_params.h | 3 +- .../hcs/hyperv_hcs_network_adapter.cpp | 49 +++++++++++++++++++ .../hcs/hyperv_hcs_network_adapter.h | 48 ++++++++++++++++++ .../hyperv_api/hcs_virtual_machine.cpp | 18 +++---- tests/hyperv_api/test_bb_cit_hyperv.cpp | 15 +++--- 7 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.cpp create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index bf7a9a670f..bae37ae561 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -48,6 +48,7 @@ if(WIN32) hcn/hyperv_hcn_network_policy.cpp hcs/hyperv_hcs_api_wrapper.cpp hcs/hyperv_hcs_scsi_device.cpp + hcs/hyperv_hcs_network_adapter.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 8cfc9f3532..4228030c38 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -223,26 +223,6 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - const auto network_adapters = [&]() { - std::vector network_adapters = {}; - - constexpr auto network_adapter_template = LR"( - "{0}": {{ - "EndpointId" : "{0}", - "MacAddress": "{1}" - }})"; - - for (const auto& endpoint : params.endpoints) - { - network_adapters.push_back(fmt::format(network_adapter_template, - maybe_widen{endpoint.endpoint_guid}, - maybe_widen{endpoint.nic_mac_address})); - } - - return fmt::format(L"{}", fmt::join(network_adapters, L", ")); - ; - }(); - const auto plan9_shares = [&]() { std::vector plan9_shares = {}; @@ -331,7 +311,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam params.memory_size_mb, maybe_widen{params.name}, fmt::join(params.scsi_devices, L","), - network_adapters, + fmt::join(params.network_adapters, L","), plan9_shares); // FIXME: Replace this with wide-string logging API when it's available. diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index e74ad8c124..22400b9a5f 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -58,7 +59,7 @@ struct CreateComputeSystemParameters * List of endpoints that'll be added to the compute system * by default at creation time. */ - std::vector endpoints{}; + std::vector network_adapters{}; /** * List of Plan9 shares that'll be added to the compute system diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.cpp new file mode 100644 index 0000000000..47ec1ad75e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsNetworkAdapter; + +template +template +auto fmt::formatter::format(const HcsNetworkAdapter& network_adapter, FormatContext& ctx) const + -> typename FormatContext::iterator +{ + constexpr static auto network_adapter_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + "{0}": {{ + "EndpointId" : "{0}", + "MacAddress": "{1}", + "InstanceId": "{0}" + }} + )json"); + + return format_to(ctx.out(), + network_adapter_template.as(), + maybe_widen{network_adapter.endpoint_guid}, + maybe_widen{network_adapter.mac_address}); +} + +template auto fmt::formatter::format(const HcsNetworkAdapter&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const HcsNetworkAdapter&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.h new file mode 100644 index 0000000000..1861d32961 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.h @@ -0,0 +1,48 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_NETWORK_ADAPTER_H +#define MULTIPASS_HYPERV_API_HCS_NETWORK_ADAPTER_H + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsNetworkAdapter +{ + std::string instance_guid; + std::string endpoint_guid; + std::string mac_address; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for HcnNetworkPolicy + */ +template +struct fmt::formatter : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsNetworkAdapter& policy, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +#endif // MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index b4ea6822f4..3ffc42a5c6 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -310,22 +310,21 @@ bool HCSVirtualMachine::maybe_create_compute_system() cloudinit_iso.read_only = true; params.scsi_devices.push_back(cloudinit_iso); - const auto create_to_add = [this](const auto& create_params) { - hcs::AddEndpointParameters add_params{}; - add_params.endpoint_guid = create_params.endpoint_guid; + const auto create_endpoint_to_network_adapter = [this](const auto& create_params) { + hcs::HcsNetworkAdapter network_adapter{}; + network_adapter.endpoint_guid = create_params.endpoint_guid; if (!create_params.mac_address) { throw CreateEndpointException("One of the endpoints do not have a MAC address!"); } - add_params.nic_mac_address = create_params.mac_address.value(); - add_params.target_compute_system_name = vm_name; - return add_params; + network_adapter.mac_address = create_params.mac_address.value(); + return network_adapter; }; std::transform(create_endpoint_params.begin(), create_endpoint_params.end(), - std::back_inserter(params.endpoints), - create_to_add); + std::back_inserter(params.network_adapters), + create_endpoint_to_network_adapter); return params; }(); @@ -336,7 +335,8 @@ bool HCSVirtualMachine::maybe_create_compute_system() } // Grant access to the VHDX and the cloud-init ISO files. - for(const auto & scsi : create_compute_system_params.scsi_devices){ + for (const auto& scsi : create_compute_system_params.scsi_devices) + { grant_access_to_paths({scsi.path}); } diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index 9a6110387a..e580061e4e 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -70,20 +70,19 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) return create_disk_parameters; }(); - const auto add_endpoint_parameters = [&endpoint_parameters]() { - hyperv::hcs::AddEndpointParameters add_endpoint_parameters{}; - add_endpoint_parameters.endpoint_guid = endpoint_parameters.endpoint_guid; - add_endpoint_parameters.target_compute_system_name = "multipass-hyperv-cit-vm"; - add_endpoint_parameters.nic_mac_address = "00-15-5D-9D-CF-69"; - return add_endpoint_parameters; + 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 = [&add_endpoint_parameters]() { + 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.endpoints.push_back(add_endpoint_parameters); + vm_parameters.network_adapters.push_back(network_adapter); return vm_parameters; }(); From 928e28093f457d4b8a019c32b4404d17cef091d6 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 18:39:17 +0300 Subject: [PATCH 57/76] [hyperv-hcs] replace AddEndpointParameters with HcsNetworkAdapter - add_endpoint function now takes compute system name as the rest of the API does - Updated code references --- .../hcs/hyperv_hcs_add_endpoint_params.h | 73 ------------------- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 14 +--- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 6 +- .../hyperv_hcs_create_compute_system_params.h | 1 - .../hcs/hyperv_hcs_wrapper_interface.h | 5 +- tests/hyperv_api/test_bb_cit_hyperv.cpp | 19 +++-- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 37 ++++------ 7 files changed, 34 insertions(+), 121 deletions(-) delete mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_add_endpoint_params.h diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_add_endpoint_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_add_endpoint_params.h deleted file mode 100644 index 198eb39f8e..0000000000 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_add_endpoint_params.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 . - * - */ - -#ifndef MULTIPASS_HYPERV_API_HCS_ADD_ENDPOINT_PARAMETERS_H -#define MULTIPASS_HYPERV_API_HCS_ADD_ENDPOINT_PARAMETERS_H - -#include -#include - -namespace multipass::hyperv::hcs -{ - -/** - * Parameters for adding a network endpoint to - * a Host Compute System.. - */ -struct AddEndpointParameters -{ - /** - * Name of the target host compute system - */ - std::string target_compute_system_name{}; - - /** - * GUID of the endpoint to add. - */ - std::string endpoint_guid{}; - - /** - * MAC address to assign to the NIC - */ - std::string nic_mac_address{}; -}; - -} // namespace multipass::hyperv::hcs - -/** - * Formatter type specialization for CreateComputeSystemParameters - */ -template -struct fmt::formatter -{ - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - - template - auto format(const multipass::hyperv::hcs::AddEndpointParameters& params, FormatContext& ctx) const - { - return format_to(ctx.out(), - "Host Compute System Name: ({}) | Endpoint GUID: ({}) | NIC MAC Address: ({})", - params.target_compute_system_name, - params.endpoint_guid, - params.nic_mac_address); - } -}; - -#endif // MULTIPASS_HYPERV_API_HCS_ADD_ENDPOINT_PARAMETERS_H diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 4228030c38..95bba2701d 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -16,7 +16,6 @@ */ #include -#include #include #include #include @@ -396,7 +395,7 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::add_endpoint(const AddEndpointParameters& params) const +OperationResult HCSWrapper::add_endpoint(const std::string& compute_system_name, const HcsNetworkAdapter& params) const { mpl::debug(kLogCategory, "add_endpoint(...) > params: {}", params); constexpr auto add_endpoint_settings_template = LR"( @@ -410,15 +409,10 @@ OperationResult HCSWrapper::add_endpoint(const AddEndpointParameters& params) co }} }})"; - const auto settings = fmt::format(add_endpoint_settings_template, - maybe_widen{params.endpoint_guid}, - maybe_widen{params.nic_mac_address}); + const auto settings = + fmt::format(add_endpoint_settings_template, maybe_widen{params.endpoint_guid}, maybe_widen{params.mac_address}); - return perform_hcs_operation(api, - api.ModifyComputeSystem, - params.target_compute_system_name, - settings.c_str(), - nullptr); + return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } // --------------------------------------------------------- diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index 110019fcf7..f9e736d3d4 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -167,12 +167,14 @@ struct HCSWrapper : public HCSWrapperInterface * the endpoint. The network interface card's name will be the * endpoint's GUID for convenience. * - * @param [in] params Endpoint parameters + * @param [in] compute_system_name Target compute system's name + * @param [in] params Network adapter parameters * * @return An object that evaluates to true on success, false otherwise. * message() may contain details of failure when result is false. */ - [[nodiscard]] OperationResult add_endpoint(const AddEndpointParameters& params) const override; + [[nodiscard]] OperationResult add_endpoint(const std::string& compute_system_name, + const HcsNetworkAdapter& params) const override; // --------------------------------------------------------- diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 22400b9a5f..3e185e3b00 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -18,7 +18,6 @@ #ifndef MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H #define MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H -#include #include #include #include diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index f6f56695cb..1b61ee1aa6 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -18,9 +18,9 @@ #ifndef MULTIPASS_HYPERV_API_HCS_WRAPPER_INTERFACE_H #define MULTIPASS_HYPERV_API_HCS_WRAPPER_INTERFACE_H -#include #include #include +#include #include #include @@ -46,7 +46,8 @@ struct HCSWrapperInterface const std::filesystem::path& file_path) const = 0; virtual OperationResult revoke_vm_access(const std::string& compute_system_name, const std::filesystem::path& file_path) const = 0; - virtual OperationResult add_endpoint(const AddEndpointParameters& params) const = 0; + virtual OperationResult add_endpoint(const std::string& compute_system_name, + const HcsNetworkAdapter& params) const = 0; virtual OperationResult remove_endpoint(const std::string& compute_system_name, const std::string& endpoint_guid) const = 0; virtual OperationResult resize_memory(const std::string& compute_system_name, diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index e580061e4e..038ed8c09f 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -158,15 +158,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo return create_disk_parameters; }(); - const auto add_endpoint_parameters = [&endpoint_parameters]() { - hyperv::hcs::AddEndpointParameters add_endpoint_parameters{}; - add_endpoint_parameters.endpoint_guid = endpoint_parameters.endpoint_guid; - add_endpoint_parameters.target_compute_system_name = "multipass-hyperv-cit-vm"; - add_endpoint_parameters.nic_mac_address = "00-15-5D-9D-CF-69"; - return add_endpoint_parameters; - }(); - - const auto create_vm_parameters = [&add_endpoint_parameters]() { + const auto create_vm_parameters = []() { hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; vm_parameters.name = "multipass-hyperv-cit-vm"; vm_parameters.processor_count = 1; @@ -174,6 +166,13 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo return vm_parameters; }(); + 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; + }(); + // Remove remnants from previous tests, if any. { if (hcn.delete_endpoint(endpoint_parameters.endpoint_guid)) @@ -228,7 +227,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo // Add endpoint { - const auto& [status, status_msg] = hcs.add_endpoint(add_endpoint_parameters); + const auto& [status, status_msg] = hcs.add_endpoint(create_vm_parameters.name, network_adapter); ASSERT_TRUE(status); ASSERT_TRUE(status_msg.empty()); } diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 4eac4d2b36..65ab78b41f 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -15,7 +15,6 @@ * */ -#include "hyperv_api/hcs/hyperv_hcs_add_endpoint_params.h" #include "hyperv_api/hcs/hyperv_hcs_api_wrapper.h" #include "hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h" #include "hyperv_test_utils.h" @@ -2270,15 +2269,11 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_happy_path) generic_operation_happy_path( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "add_endpoint(...) > params: Host Compute System Name: (test_vm) | Endpoint GUID: " - "(288cc1ac-8f31-4a09-9e90-30ad0bcfdbca) | NIC MAC Address: (00:00:00:00:00:00)"); - hyperv::hcs::AddEndpointParameters params{}; + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...) > params:"); + hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; - params.nic_mac_address = "00:00:00:00:00:00"; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + params.mac_address = "00:00:00:00:00:00"; + return wrapper.add_endpoint("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2297,9 +2292,8 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_hcs_open_fail) mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); - hyperv::hcs::AddEndpointParameters params{}; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + hyperv::hcs::HcsNetworkAdapter params{}; + return wrapper.add_endpoint("test_vm", params); }); } @@ -2311,9 +2305,8 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_create_operation_f mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); - hyperv::hcs::AddEndpointParameters params{}; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + hyperv::hcs::HcsNetworkAdapter params{}; + return wrapper.add_endpoint("test_vm", params); }); } @@ -2336,11 +2329,10 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_fail) mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); - hyperv::hcs::AddEndpointParameters params{}; + hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; - params.nic_mac_address = "00:00:00:00:00:00"; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + params.mac_address = "00:00:00:00:00:00"; + return wrapper.add_endpoint("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2370,11 +2362,10 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_wait_for_operation mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); - hyperv::hcs::AddEndpointParameters params{}; + hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; - params.nic_mac_address = "00:00:00:00:00:00"; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + params.mac_address = "00:00:00:00:00:00"; + return wrapper.add_endpoint("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); From 84bd0962202fffc48d396ba3677e8c6d4d06c509 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 22:06:51 +0300 Subject: [PATCH 58/76] [hyperv-hcn] rename {add,rm}_endpoint to {add,rm}_network_adapter --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 19 +++--- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 12 ++-- .../hcs/hyperv_hcs_wrapper_interface.h | 8 +-- tests/hyperv_api/test_bb_cit_hyperv.cpp | 2 +- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 60 +++++++++---------- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 95bba2701d..4838de4502 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -395,10 +395,11 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::add_endpoint(const std::string& compute_system_name, const HcsNetworkAdapter& params) const +OperationResult HCSWrapper::add_network_adapter(const std::string& compute_system_name, + const HcsNetworkAdapter& params) const { - mpl::debug(kLogCategory, "add_endpoint(...) > params: {}", params); - constexpr auto add_endpoint_settings_template = LR"( + mpl::debug(kLogCategory, "add_network_adapter(...) > params: {}", params); + constexpr auto settings_template = LR"( {{ "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", "RequestType": "Add", @@ -410,28 +411,28 @@ OperationResult HCSWrapper::add_endpoint(const std::string& compute_system_name, }})"; const auto settings = - fmt::format(add_endpoint_settings_template, maybe_widen{params.endpoint_guid}, maybe_widen{params.mac_address}); + fmt::format(settings_template, maybe_widen{params.endpoint_guid}, maybe_widen{params.mac_address}); return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } // --------------------------------------------------------- -OperationResult HCSWrapper::remove_endpoint(const std::string& compute_system_name, - const std::string& endpoint_guid) const +OperationResult HCSWrapper::remove_network_adapter(const std::string& compute_system_name, + const std::string& endpoint_guid) const { mpl::debug(kLogCategory, - "remove_endpoint(...) > name: ({}), endpoint_guid: ({})", + "remove_network_adapter(...) > name: ({}), endpoint_guid: ({})", compute_system_name, endpoint_guid); - constexpr auto remove_endpoint_settings_template = LR"( + constexpr auto settings_template = LR"( {{ "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", "RequestType": "Remove" }})"; - const auto settings = fmt::format(remove_endpoint_settings_template, maybe_widen{endpoint_guid}); + const auto settings = fmt::format(settings_template, maybe_widen{endpoint_guid}); return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index f9e736d3d4..d5cd63d461 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -161,7 +161,7 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- /** - * Add a network endpoint to the host compute system. + * Add a network adapter to the host compute system. * * A new network interface card will be automatically created for * the endpoint. The network interface card's name will be the @@ -173,13 +173,13 @@ struct HCSWrapper : public HCSWrapperInterface * @return An object that evaluates to true on success, false otherwise. * message() may contain details of failure when result is false. */ - [[nodiscard]] OperationResult add_endpoint(const std::string& compute_system_name, - const HcsNetworkAdapter& params) const override; + [[nodiscard]] OperationResult add_network_adapter(const std::string& compute_system_name, + const HcsNetworkAdapter& params) const override; // --------------------------------------------------------- /** - * Remove a network endpoint from the host compute system. + * Remove a network adapter from the host compute system. * * @param [in] name Target compute system's name * @param [in] endpoint_guid GUID of the endpoint to remove @@ -187,8 +187,8 @@ struct HCSWrapper : public HCSWrapperInterface * @return An object that evaluates to true on success, false otherwise. * message() may contain details of failure when result is false. */ - [[nodiscard]] OperationResult remove_endpoint(const std::string& compute_system_name, - const std::string& endpoint_guid) const override; + [[nodiscard]] OperationResult remove_network_adapter(const std::string& compute_system_name, + const std::string& endpoint_guid) const override; // --------------------------------------------------------- diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index 1b61ee1aa6..cd356eb112 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -46,10 +46,10 @@ struct HCSWrapperInterface const std::filesystem::path& file_path) const = 0; virtual OperationResult revoke_vm_access(const std::string& compute_system_name, const std::filesystem::path& file_path) const = 0; - virtual OperationResult add_endpoint(const std::string& compute_system_name, - const HcsNetworkAdapter& params) const = 0; - virtual OperationResult remove_endpoint(const std::string& compute_system_name, - const std::string& endpoint_guid) const = 0; + virtual OperationResult add_network_adapter(const std::string& compute_system_name, + const HcsNetworkAdapter& params) const = 0; + virtual OperationResult remove_network_adapter(const std::string& compute_system_name, + const std::string& endpoint_guid) const = 0; virtual OperationResult resize_memory(const std::string& compute_system_name, const std::uint32_t new_size_mib) const = 0; virtual OperationResult update_cpu_count(const std::string& compute_system_name, diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index 038ed8c09f..25741a7104 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -227,7 +227,7 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo // Add endpoint { - const auto& [status, status_msg] = hcs.add_endpoint(create_vm_parameters.name, network_adapter); + const auto& [status, status_msg] = hcs.add_network_adapter(create_vm_parameters.name, network_adapter); ASSERT_TRUE(status); ASSERT_TRUE(status_msg.empty()); } diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 65ab78b41f..8c8053cd61 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -2253,7 +2253,7 @@ TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_wait_for_operation_result_f // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_happy_path) +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_happy_path) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2269,11 +2269,11 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_happy_path) generic_operation_happy_path( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...) > params:"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...) > params:"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_endpoint("test_vm", params); + return wrapper.add_network_adapter("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2286,33 +2286,33 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_happy_path) // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_hcs_open_fail) +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_hcs_open_fail) { generic_operation_hcs_open_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); hyperv::hcs::HcsNetworkAdapter params{}; - return wrapper.add_endpoint("test_vm", params); + return wrapper.add_network_adapter("test_vm", params); }); } // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_create_operation_fail) +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_create_operation_fail) { generic_operation_create_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); hyperv::hcs::HcsNetworkAdapter params{}; - return wrapper.add_endpoint("test_vm", params); + return wrapper.add_network_adapter("test_vm", params); }); } // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_fail) +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_fail) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2328,11 +2328,11 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_fail) generic_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_endpoint("test_vm", params); + return wrapper.add_network_adapter("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2345,7 +2345,7 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_fail) // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_wait_for_operation_result_fail) +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_wait_for_operation_result_fail) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2361,11 +2361,11 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_wait_for_operation generic_operation_wait_for_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_endpoint(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_endpoint("test_vm", params); + return wrapper.add_network_adapter("test_vm", params); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2378,7 +2378,7 @@ TEST_F(HyperVHCSAPI_UnitTests, add_endpoint_to_compute_system_wait_for_operation // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_happy_path) +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_happy_path) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2391,8 +2391,8 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_happy_path) [&](hyperv::hcs::HCSWrapper& wrapper) { logger_scope.mock_logger->expect_log( mpl::Level::debug, - "remove_endpoint(...) > name: (test_vm), endpoint_guid: (288cc1ac-8f31-4a09-9e90-30ad0bcfdbca)"); - return wrapper.remove_endpoint("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + "remove_network_adapter(...) > name: (test_vm), endpoint_guid: (288cc1ac-8f31-4a09-9e90-30ad0bcfdbca)"); + return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2405,31 +2405,31 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_happy_path) // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_hcs_open_fail) +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_hcs_open_fail) { generic_operation_hcs_open_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_endpoint(...)"); - return wrapper.remove_endpoint("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); + return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); }); } // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_create_operation_fail) +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_create_operation_fail) { generic_operation_create_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_endpoint(...)"); - return wrapper.remove_endpoint("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); + return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); }); } // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_fail) +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_fail) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2440,8 +2440,8 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_fail) generic_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_endpoint(...)"); - return wrapper.remove_endpoint("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); + return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2454,7 +2454,7 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_fail) // --------------------------------------------------------- -TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_wait_for_operation_result_fail) +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_wait_for_operation_result_fail) { constexpr auto expected_modify_compute_system_configuration = LR"( { @@ -2465,8 +2465,8 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_endpoint_from_compute_system_wait_for_oper generic_operation_wait_for_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_endpoint(...)"); - return wrapper.remove_endpoint("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); + return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); From bbc9aced55458788c16e54f67651b10ee5715bc3 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 22:15:47 +0300 Subject: [PATCH 59/76] [hyperv-hcs] move plan9 shares formatting to its own source --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 30 +--------- .../hcs/hyperv_hcs_plan9_share_params.cpp | 56 +++++++++++++++++++ .../hcs/hyperv_hcs_plan9_share_params.h | 21 ++----- 4 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index bae37ae561..a0b4e57808 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -49,6 +49,7 @@ if(WIN32) hcs/hyperv_hcs_api_wrapper.cpp hcs/hyperv_hcs_scsi_device.cpp hcs/hyperv_hcs_network_adapter.cpp + hcs/hyperv_hcs_plan9_share_params.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 4838de4502..d38efdeaf4 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -222,34 +222,6 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - const auto plan9_shares = [&]() { - std::vector plan9_shares = {}; - - constexpr auto plan9_share_template = LR"( - {{ - "Name": "{0}", - "Path": "{1}", - "Port": {2}, - "AccessName": "{3}" - }} - )"; - - for (const auto& share : params.shares) - { - plan9_shares.push_back(fmt::format(plan9_share_template, - maybe_widen{share.name}, - share.host_path.wstring(), - share.port, - maybe_widen{share.access_name}, - fmt::underlying(share.flags))); - } - - return fmt::format(L"{}", fmt::join(plan9_shares, L", ")); - }(); - - // Ideally, we should codegen from the schema - // and use that. - // https://raw.githubusercontent.com/MicrosoftDocs/Virtualization-Documentation/refs/heads/main/hyperv-samples/hcs-samples/JSON_files/HCS_Schema%5BWindows_10_SDK_version_1809%5D.json constexpr auto vm_settings_template = LR"( {{ "SchemaVersion": {{ @@ -311,7 +283,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam maybe_widen{params.name}, fmt::join(params.scsi_devices, L","), fmt::join(params.network_adapters, L","), - plan9_shares); + fmt::join(params.shares, L",")); // FIXME: Replace this with wide-string logging API when it's available. fmt::print(L"Rendered VM settings document: \n{}\n", vm_settings); diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp new file mode 100644 index 0000000000..5749b2e34e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include + +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::Plan9ShareParameters; + +template +template +auto fmt::formatter::format(const Plan9ShareParameters& params, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "Name": "{0}", + "Path": "{1}", + "Port": {2}, + "AccessName": "{3}", + "Flags": "{4}" + }} + )json"); + + return format_to(ctx.out(), + json_template.as(), + maybe_widen{params.name}, + params.host_path, + params.port, + maybe_widen{params.access_name}, + fmt::underlying(params.flags)); +} + +template auto fmt::formatter::format(const Plan9ShareParameters&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const Plan9ShareParameters&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h index 79e91beb85..7ba2d30e82 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -78,27 +78,14 @@ struct Plan9ShareParameters } // namespace multipass::hyperv::hcs /** - * Formatter type specialization for CreateComputeSystemParameters + * Formatter type specialization for Plan9ShareParameters */ template -struct fmt::formatter +struct fmt::formatter : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcs::Plan9ShareParameters& params, FormatContext& ctx) const - { - return format_to(ctx.out(), - "Share name: ({}) | Access name: ({}) | Host path: ({}) | Port: ({}) ", - params.name, - params.access_name, - params.host_path, - params.port, - fmt::underlying(params.flags)); - } + auto format(const multipass::hyperv::hcs::Plan9ShareParameters& policy, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif // MULTIPASS_HYPERV_API_HCS_ADD_9P_SHARE_PARAMS_H From d54df95a51134c65a83d9b4220a424e4f0726a70 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 22:54:40 +0300 Subject: [PATCH 60/76] [hyperv-hcs] move CreateComputeSystemParams formatting to src/ --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 62 +---------- ...yperv_hcs_create_compute_system_params.cpp | 102 ++++++++++++++++++ .../hyperv_hcs_create_compute_system_params.h | 18 +--- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 20 +--- 5 files changed, 112 insertions(+), 91 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.cpp diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index a0b4e57808..bed801d475 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -50,6 +50,7 @@ if(WIN32) hcs/hyperv_hcs_scsi_device.cpp hcs/hyperv_hcs_network_adapter.cpp hcs/hyperv_hcs_plan9_share_params.cpp + hcs/hyperv_hcs_create_compute_system_params.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index d38efdeaf4..d4ee6518c2 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -222,68 +222,8 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - constexpr auto vm_settings_template = LR"( - {{ - "SchemaVersion": {{ - "Major": 2, - "Minor": 1 - }}, - "Owner": "Multipass", - "ShouldTerminateOnLastHandleClosed": false, - "VirtualMachine": {{ - "Chipset": {{ - "Uefi": {{ - "BootThis": {{ - "DevicePath": "Primary disk", - "DiskNumber": 0, - "DeviceType": "ScsiDrive" - }}, - "Console": "ComPort1" - }} - }}, - "ComputeTopology": {{ - "Memory": {{ - "Backing": "Virtual", - "SizeInMB": {1} - }}, - "Processor": {{ - "Count": {0} - }} - }}, - "Devices": {{ - "ComPorts": {{ - "0": {{ - "NamedPipe": "\\\\.\\pipe\\{2}" - }} - }}, - "Scsi": {{ - {3} - }}, - "NetworkAdapters": {{ - {4} - }}, - "Plan9": {{ - "Shares": [ - {5} - ] - }} - }}, - "Services": {{ - "Shutdown": {{}}, - "Heartbeat": {{}} - }} - }} - }})"; - // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#HvSocketSystemConfig - // Render the template - const auto vm_settings = fmt::format(vm_settings_template, - params.processor_count, - params.memory_size_mb, - maybe_widen{params.name}, - fmt::join(params.scsi_devices, L","), - fmt::join(params.network_adapters, L","), - fmt::join(params.shares, L",")); + const auto vm_settings = fmt::format(L"{}", params); // FIXME: Replace this with wide-string logging API when it's available. fmt::print(L"Rendered VM settings document: \n{}\n", vm_settings); diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.cpp new file mode 100644 index 0000000000..3a8ed8e1f9 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::CreateComputeSystemParameters; + +template +template +auto fmt::formatter::format(const CreateComputeSystemParameters& params, + FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "SchemaVersion": {{ + "Major": 2, + "Minor": 1 + }}, + "Owner": "Multipass", + "ShouldTerminateOnLastHandleClosed": false, + "VirtualMachine": {{ + "Chipset": {{ + "Uefi": {{ + "BootThis": {{ + "DevicePath": "Primary disk", + "DiskNumber": 0, + "DeviceType": "ScsiDrive" + }}, + "Console": "ComPort1" + }} + }}, + "ComputeTopology": {{ + "Memory": {{ + "Backing": "Virtual", + "SizeInMB": {1} + }}, + "Processor": {{ + "Count": {0} + }} + }}, + "Devices": {{ + "ComPorts": {{ + "0": {{ + "NamedPipe": "\\\\.\\pipe\\{2}" + }} + }}, + "Scsi": {{ + {3} + }}, + "NetworkAdapters": {{ + {4} + }}, + "Plan9": {{ + "Shares": [ + {5} + ] + }} + }}, + "Services": {{ + "Shutdown": {{}}, + "Heartbeat": {{}} + }} + }} + }} + )json"); + + constexpr static auto comma = MULTIPASS_UNIVERSAL_LITERAL(","); + + return format_to(ctx.out(), + json_template.as(), + params.processor_count, + params.memory_size_mb, + maybe_widen{params.name}, + fmt::join(params.scsi_devices, comma.as()), + fmt::join(params.network_adapters, comma.as()), + fmt::join(params.shares, comma.as())); +} + +template auto fmt::formatter::format( + const CreateComputeSystemParameters&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const CreateComputeSystemParameters&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 3e185e3b00..2ebaaf9392 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -18,9 +18,9 @@ #ifndef MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H #define MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H +#include #include #include -#include #include #include @@ -74,21 +74,11 @@ struct CreateComputeSystemParameters */ template struct fmt::formatter + : formatter, Char> { - constexpr auto parse(basic_format_parse_context& ctx) - { - return ctx.begin(); - } - template - auto format(const multipass::hyperv::hcs::CreateComputeSystemParameters& params, FormatContext& ctx) const - { - return format_to(ctx.out(), - "Compute System name: ({}) | vCPU count: ({}) | Memory size: ({} MiB)", - params.name, - params.processor_count, - params.memory_size_mb); - } + auto format(const multipass::hyperv::hcs::CreateComputeSystemParameters& policy, FormatContext& ctx) const -> + typename FormatContext::iterator; }; #endif // MULTIPASS_HYPERV_API_HCS_CREATE_COMPUTE_SYSTEM_PARAMETERS_H diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 8c8053cd61..d8dd2852ac 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -307,10 +307,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -478,10 +475,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -645,10 +639,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -804,10 +795,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "HCSWrapper::create_compute_system(...) > params: Compute System name: (test_vm) | vCPU count: (8) | " - "Memory size: (16384 MiB)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); From 19ece869cbaac3ba071390bfd0df0869f44b6e4c Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 7 May 2025 23:52:51 +0300 Subject: [PATCH 61/76] [hyperv-hcs-hcn] replace fmt::format("{}", v) with fmt::to_string --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 68 ++++++++++--------- tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 2 +- tests/hyperv_api/test_ut_hyperv_hcn_api.cpp | 30 +++----- tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp | 6 +- .../test_ut_hyperv_hcn_network_policy.cpp | 4 +- tests/hyperv_api/test_ut_hyperv_hcn_route.cpp | 4 +- .../hyperv_api/test_ut_hyperv_hcn_subnet.cpp | 4 +- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 22 +++--- 8 files changed, 66 insertions(+), 74 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index d4ee6518c2..4c9710e5f5 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -68,7 +68,7 @@ constexpr auto kDefaultOperationTimeout = std::chrono::seconds{240}; */ UniqueHcsOperation create_operation(const HCSAPITable& api) { - mpl::debug(kLogCategory, "create_operation(...)"); + mpl::trace(kLogCategory, "create_operation(...)"); return UniqueHcsOperation{api.CreateOperation(nullptr, nullptr), api.CloseOperation}; } @@ -127,7 +127,7 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri constexpr auto kRequestedAccessLevel = GENERIC_ALL; HCS_SYSTEM system{nullptr}; - const auto result = ResultCode{api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, &system)}; + const ResultCode result = api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, &system); if (!result) { @@ -141,6 +141,34 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri // --------------------------------------------------------- +template +OperationResult perform_hcs_operation(const HCSAPITable& api, const FnType& fn, UniqueHcsSystem system, Args&&... args) +{ + auto operation = create_operation(api); + + if (nullptr == operation) + { + mpl::error(kLogCategory, "perform_hcs_operation(...) > HcsCreateOperation failed! "); + return OperationResult{E_POINTER, L"HcsCreateOperation failed!"}; + } + + // Perform the operation. + const auto result = ResultCode{fn(system.get(), operation.get(), std::forward(args)...)}; + + if (!result) + { + mpl::error(kLogCategory, "perform_hcs_operation(...) > Operation failed! Result code {}", result); + return OperationResult{result, L"HCS operation failed!"}; + } + + mpl::debug(kLogCategory, + "perform_hcs_operation(...) > fn: {}, result: {}", + fmt::ptr(fn.template target()), + static_cast(result)); + + return wait_for_operation_result(api, std::move(operation)); +} + /** * Perform a Host Compute System API operation. * @@ -170,7 +198,7 @@ OperationResult perform_hcs_operation(const HCSAPITable& api, return {E_POINTER, L"Operation function is unbound!"}; } - const auto system = open_host_compute_system(api, target_hcs_system_name); + auto system = open_host_compute_system(api, target_hcs_system_name); if (nullptr == system) { @@ -180,31 +208,7 @@ OperationResult perform_hcs_operation(const HCSAPITable& api, return OperationResult{E_INVALIDARG, L"HcsOpenComputeSystem failed!"}; } - auto operation = create_operation(api); - - if (nullptr == operation) - { - mpl::error(kLogCategory, "perform_hcs_operation(...) > HcsCreateOperation failed! {}", target_hcs_system_name); - return OperationResult{E_POINTER, L"HcsCreateOperation failed!"}; - } - - const auto result = ResultCode{fn(system.get(), operation.get(), std::forward(args)...)}; - - if (!result) - { - mpl::error(kLogCategory, - "perform_hcs_operation(...) > Operation failed! {} Result code {}", - target_hcs_system_name, - result); - return OperationResult{result, L"HCS operation failed!"}; - } - - mpl::debug(kLogCategory, - "perform_hcs_operation(...) > fn: {}, result: {}", - fmt::ptr(fn.template target()), - static_cast(result)); - - return wait_for_operation_result(api, std::move(operation)); + return perform_hcs_operation(api, fn, std::move(system), std::forward(args)...); } } // namespace @@ -222,11 +226,6 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - // Render the template - const auto vm_settings = fmt::format(L"{}", params); - - // FIXME: Replace this with wide-string logging API when it's available. - fmt::print(L"Rendered VM settings document: \n{}\n", vm_settings); HCS_SYSTEM system{nullptr}; auto operation = create_operation(api); @@ -237,10 +236,13 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam } const std::wstring name_w = maybe_widen{params.name}; + // Render the template + const auto vm_settings = fmt::to_wstring(params); const auto result = ResultCode{api.CreateComputeSystem(name_w.c_str(), vm_settings.c_str(), operation.get(), nullptr, &system)}; // Auto-release the system handle + // FIXME: std::out_ptr with C++23 [[maybe_unused]] UniqueHcsSystem _{system, api.CloseComputeSystem}; if (!result) diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 9c0945c778..038d020a4e 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -157,7 +157,7 @@ TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) const auto u_result = uut.update_cpu_count(params.name, 8); EXPECT_TRUE(u_result); - auto v = fmt::format("{}", u_result.code); + auto v = fmt::to_string(u_result.code); std::wprintf(L"%s\n", u_result.status_msg.c_str()); std::printf("%s \n", v.c_str()); diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index 1d299c37ed..f168f63ed1 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -165,8 +165,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_ics) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); *network = mock_network_object; }, Return(NOERROR))); @@ -244,8 +243,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_transparent) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); *network = mock_network_object; }, Return(NOERROR))); @@ -334,8 +332,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_with_flags_multiple_polici const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); *network = mock_network_object; }, Return(NOERROR))); @@ -432,8 +429,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success_multiple_ipams) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_network_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); *network = mock_network_object; }, Return(NOERROR))); @@ -602,8 +598,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_network_success) EXPECT_CALL(mock_delete_network, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = fmt::format("{}", guid); - ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(guid)); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); }, @@ -728,8 +723,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto endpoint_guid_str = fmt::format("{}", id); - ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", endpoint_guid_str); + ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", fmt::to_string(id)); *endpoint = mock_endpoint_object; }, Return(NOERROR))); @@ -740,8 +734,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); ASSERT_NE(nullptr, network); ASSERT_EQ(nullptr, *network); ASSERT_NE(nullptr, error_record); @@ -875,8 +868,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) const auto config_no_whitespace = trim_whitespace(settings); const auto expected_no_whitespace = trim_whitespace(expected_endpoint_settings); ASSERT_STREQ(config_no_whitespace.c_str(), expected_no_whitespace.c_str()); - const auto expected_endpoint_guid_str = fmt::format("{}", id); - ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", expected_endpoint_guid_str); + ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", fmt::to_string(id)); *endpoint = mock_endpoint_object; *error_record = mock_error_msg; }, @@ -888,8 +880,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) EXPECT_CALL(mock_open_network, Call) .WillOnce(DoAll( [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { - const auto expected_network_guid_str = fmt::format("{}", id); - ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", expected_network_guid_str); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); ASSERT_NE(nullptr, error_record); ASSERT_EQ(nullptr, *error_record); *network = mock_network_object; @@ -950,8 +941,7 @@ TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_success) EXPECT_CALL(mock_delete_endpoint, Call) .WillOnce(DoAll( [&](REFGUID guid, PWSTR* error_record) { - const auto guid_str = fmt::format("{}", guid); - ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", guid_str); + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(guid)); ASSERT_EQ(nullptr, *error_record); ASSERT_NE(nullptr, error_record); }, diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp index 5e0b37fb6c..c98beeff99 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_ipam.cpp @@ -42,7 +42,7 @@ TEST_F(HyperVHCNIpam_UnitTests, format_narrow) uut.type = hyperv::hcn::HcnIpamType::Static(); uut.subnets.emplace_back( hyperv::hcn::HcnSubnet{"192.168.1.0/24", {hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}}}); - const auto result = fmt::format("{}", uut); + const auto result = fmt::to_string(uut); constexpr auto expected_result = R"json( { "Type": "static", @@ -79,8 +79,8 @@ TEST_F(HyperVHCNIpam_UnitTests, format_wide) uut.type = hyperv::hcn::HcnIpamType::Dhcp(); uut.subnets.emplace_back( hyperv::hcn::HcnSubnet{"192.168.1.0/24", {hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}}}); - const auto result = fmt::format("{}", uut); - constexpr auto expected_result = R"json( + const auto result = fmt::to_wstring(uut); + constexpr auto expected_result = LR"json( { "Type": "DHCP", "Subnets": [ diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp index 77e4b3ed2a..be50a6e873 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp @@ -41,7 +41,7 @@ TEST_F(HyperVHCNNetworkPolicy_UnitTests, format_narrow) uut_t uut{hyperv::hcn::HcnNetworkPolicyType::NetAdapterName()}; uut.settings = hyperv::hcn::HcnNetworkPolicyNetAdapterName{"client eastwood"}; - const auto result = fmt::format("{}", uut); + const auto result = fmt::to_string(uut); constexpr auto expected_result = R"json( { "Type": "NetAdapterName", @@ -66,7 +66,7 @@ TEST_F(HyperVHCNNetworkPolicy_UnitTests, format_wide) uut_t uut{hyperv::hcn::HcnNetworkPolicyType::NetAdapterName()}; uut.settings = hyperv::hcn::HcnNetworkPolicyNetAdapterName{"client eastwood"}; - const auto result = fmt::format(L"{}", uut); + const auto result = fmt::to_wstring(uut); constexpr auto expected_result = LR"json( { "Type": "NetAdapterName", diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp index 4c3cda8c4f..1e6f1d484c 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_route.cpp @@ -41,7 +41,7 @@ TEST_F(HyperVHCNRoute_UnitTests, format_narrow) uut.destination_prefix = "0.0.0.0/0"; uut.metric = 123; uut.next_hop = "192.168.1.1"; - const auto result = fmt::format("{}", uut); + const auto result = fmt::to_string(uut); constexpr auto expected_result = R"json( { "NextHop": "192.168.1.1", @@ -60,7 +60,7 @@ TEST_F(HyperVHCNRoute_UnitTests, format_wide) uut.destination_prefix = "0.0.0.0/0"; uut.metric = 123; uut.next_hop = "192.168.1.1"; - const auto result = fmt::format(L"{}", uut); + const auto result = fmt::to_wstring(uut); constexpr auto expected_result = LR"json( { "NextHop": "192.168.1.1", diff --git a/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp index 55e9491815..608e98f32a 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcn_subnet.cpp @@ -41,7 +41,7 @@ TEST_F(HyperVHCNSubnet_UnitTests, format_narrow) uut_t uut; uut.ip_address_prefix = "192.168.1.0/24"; uut.routes.emplace_back(hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}); - const auto result = fmt::format("{}", uut); + const auto result = fmt::to_string(uut); constexpr auto expected_result = R"json( { "Policies": [], @@ -72,7 +72,7 @@ TEST_F(HyperVHCNSubnet_UnitTests, format_wide) uut_t uut; uut.ip_address_prefix = "192.168.1.0/24"; uut.routes.emplace_back(hcn::HcnRoute{"192.168.1.1", "0.0.0.0/0", 123}); - const auto result = fmt::format(L"{}", uut); + const auto result = fmt::to_wstring(uut); constexpr auto expected_result = LR"json( { "Policies": [], diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index d8dd2852ac..5933debb90 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -308,7 +308,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } @@ -476,7 +476,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } @@ -640,7 +640,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } @@ -796,7 +796,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...) > params:"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } @@ -847,7 +847,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_create_operation_fail) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); } /****************************************************** @@ -997,7 +997,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); } /****************************************************** @@ -1172,7 +1172,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::create_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } @@ -1466,7 +1466,7 @@ void HyperVHCSAPI_UnitTests::generic_operation_happy_path(ApiFnT& target_api_fun logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_hcs_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); @@ -1599,7 +1599,7 @@ void HyperVHCSAPI_UnitTests::generic_operation_create_operation_fail(ApiFnT& tar logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::error, "perform_hcs_operation(...) > HcsCreateOperation failed!"); } @@ -1682,7 +1682,7 @@ void HyperVHCSAPI_UnitTests::generic_operation_fail(ApiFnT& target_api_function, logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::error, "perform_hcs_operation(...) > Operation failed!"); } @@ -1786,7 +1786,7 @@ void HyperVHCSAPI_UnitTests::generic_operation_wait_for_operation_fail(ApiFnT& t logger_scope.mock_logger->expect_log(mpl::Level::debug, "HCSWrapper::HCSWrapper(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "open_host_compute_system(...)"); - logger_scope.mock_logger->expect_log(mpl::Level::debug, "create_operation(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "create_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "perform_hcs_operation(...)"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > ("); logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); From 15fe25be6020aef10d59f1514b9d590b5c73daf9 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 9 May 2025 12:25:18 +0300 Subject: [PATCH 62/76] [hyperv-api-utils] introduce out_ptr type to simplify the interaction between the smart pointers and the C API a lean out_ptr type is implemented based on P1132R6 paper, which is already accepted into the C++23 standard. - open_host_compute_system and create_host_compute system now uses the out_ptr to avoid creating a temporary raw pointer for the API calls. --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 25 ++-- .../backends/hyperv_api/util/out_ptr.h | 110 ++++++++++++++++++ 2 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 src/platform/backends/hyperv_api/util/out_ptr.h diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 4c9710e5f5..f9fe6bbe19 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -18,9 +18,9 @@ #include #include -#include - #include +#include +#include #include @@ -126,9 +126,9 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri const std::wstring name_w = maybe_widen{name}; constexpr auto kRequestedAccessLevel = GENERIC_ALL; - HCS_SYSTEM system{nullptr}; - const ResultCode result = api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, &system); - + UniqueHcsSystem system{}; + const ResultCode result = + api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, util::out_ptr(system, api.CloseComputeSystem)); if (!result) { mpl::debug(kLogCategory, @@ -136,7 +136,7 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri name, result); } - return UniqueHcsSystem{system, api.CloseComputeSystem}; + return system; } // --------------------------------------------------------- @@ -226,8 +226,6 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam { mpl::debug(kLogCategory, "HCSWrapper::create_compute_system(...) > params: {} ", params); - HCS_SYSTEM system{nullptr}; - auto operation = create_operation(api); if (nullptr == operation) @@ -238,12 +236,13 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam const std::wstring name_w = maybe_widen{params.name}; // Render the template const auto vm_settings = fmt::to_wstring(params); - const auto result = - ResultCode{api.CreateComputeSystem(name_w.c_str(), vm_settings.c_str(), operation.get(), nullptr, &system)}; - // Auto-release the system handle - // FIXME: std::out_ptr with C++23 - [[maybe_unused]] UniqueHcsSystem _{system, api.CloseComputeSystem}; + UniqueHcsSystem system{}; + const auto result = ResultCode{api.CreateComputeSystem(name_w.c_str(), + vm_settings.c_str(), + operation.get(), + nullptr, + util::out_ptr(system, api.CloseComputeSystem))}; if (!result) { diff --git a/src/platform/backends/hyperv_api/util/out_ptr.h b/src/platform/backends/hyperv_api/util/out_ptr.h new file mode 100644 index 0000000000..1786640dd7 --- /dev/null +++ b/src/platform/backends/hyperv_api/util/out_ptr.h @@ -0,0 +1,110 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_UTIL_OUT_PTR_H +#define MULTIPASS_HYPERV_API_UTIL_OUT_PTR_H + +#include +#include + +namespace multipass::hyperv::util +{ + +namespace detail +{ +template +struct is_resettable : std::false_type +{ +}; + +template +struct is_resettable().reset(std::declval

(), std::declval()...))>, + A...> : std::true_type +{ +}; + +template +inline constexpr bool is_resettable_v = is_resettable::value; +} // namespace detail + +// A "just-enough" implementation of out_ptr +// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1132r5.html +template +class out_ptr_t +{ + Smart* sp_ = nullptr; // the smart pointer we’re updating + Pointer raw_ = nullptr; // where the C API will write the new handle + std::tuple extra_; // optional args forwarded to reset() + +public: + // Construction just remembers where to put things + out_ptr_t(Smart& s, Args&&... a) : sp_(std::addressof(s)), extra_(std::forward(a)...) + { + } + + // Non‑copyable, non‑movable – you get one shot per call site + out_ptr_t(const out_ptr_t&) = delete; + out_ptr_t& operator=(const out_ptr_t&) = delete; + out_ptr_t(out_ptr_t&&) = delete; + out_ptr_t& operator=(out_ptr_t&&) = delete; + + // The whole point: give the C API a place to stuff the pointer + operator Pointer*() const noexcept + { + return std::addressof(const_cast(raw_)); + } + operator void**() const noexcept + { + return reinterpret_cast(std::addressof(raw_)); + } + + // Cleanup: push the freshly‑filled pointer back into the smart pointer + ~out_ptr_t() + { + std::apply( + [&](auto&&... xs) { + if (sp_ && raw_) + { + if constexpr (detail::is_resettable_v) + { + sp_->reset(raw_, std::forward(xs)...); + } + else + { + *sp_ = Smart(raw_, std::forward(xs)...); + } + } + }, + extra_); + } +}; + +// Helper that lets you write std::out_ptr(up) with CTAD‑like deduction +template +[[nodiscard]] auto out_ptr(Smart& s, Args&&... a) +{ + using Ptr = typename Smart::pointer; + return out_ptr_t(s, std::forward(a)...); +} + +} // namespace multipass::hyperv::util + +#endif From 60e3b61ab35d25a825235056e116e6e634ef2a71 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 9 May 2025 12:57:15 +0300 Subject: [PATCH 63/76] [hyperv-util] gate void** operator to P != void --- src/platform/backends/hyperv_api/util/out_ptr.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/backends/hyperv_api/util/out_ptr.h b/src/platform/backends/hyperv_api/util/out_ptr.h index 1786640dd7..cd37148dd9 100644 --- a/src/platform/backends/hyperv_api/util/out_ptr.h +++ b/src/platform/backends/hyperv_api/util/out_ptr.h @@ -71,6 +71,9 @@ class out_ptr_t { return std::addressof(const_cast(raw_)); } + + template ::type, void*>::value>> operator void**() const noexcept { return reinterpret_cast(std::addressof(raw_)); From b758c02ab1c1ac44079248b4101d53133a02d754 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 9 May 2025 16:47:01 +0300 Subject: [PATCH 64/76] [hyperv-hcn] use out_ptr where appropriate --- .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index de97380d79..22bc49d5f3 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -149,16 +150,13 @@ OperationResult perform_hcn_operation(const HCNAPITable& api, const FnType& fn, // HCN functions will use CoTaskMemAlloc to allocate the error message buffer // so use UniqueCotaskmemString to auto-release it with appropriate free // function. - - wchar_t* result_msg_out{nullptr}; + UniqueCotaskmemString result_msgbuf{}; // Perform the operation. The last argument of the all HCN operations (except // HcnClose*) is ErrorRecord, which is a JSON-formatted document emitted by // the API describing the error happened. Therefore, we can streamline all API // calls through perform_operation to perform co - const auto result = ResultCode{fn(std::forward(args)..., &result_msg_out)}; - - UniqueCotaskmemString result_msgbuf{result_msg_out, api.CoTaskMemFree}; + const auto result = ResultCode{fn(std::forward(args)..., util::out_ptr(result_msgbuf, api.CoTaskMemFree))}; mpl::debug(kLogCategory, "perform_operation(...) > fn: {}, result: {}", @@ -187,14 +185,17 @@ OperationResult perform_hcn_operation(const HCNAPITable& api, const FnType& fn, UniqueHcnNetwork open_network(const HCNAPITable& api, const std::string& network_guid) { mpl::debug(kLogCategory, "open_network(...) > network_guid: {} ", network_guid); - HCN_NETWORK network{nullptr}; - const auto result = perform_hcn_operation(api, api.OpenNetwork, guid_from_string(network_guid), &network); + UniqueHcnNetwork network{}; + const auto result = perform_hcn_operation(api, + api.OpenNetwork, + guid_from_string(network_guid), + util::out_ptr(network, api.CloseNetwork)); if (!result) { mpl::error(kLogCategory, "open_network() > HcnOpenNetwork failed with {}!", result.code); } - return UniqueHcnNetwork{network, api.CloseNetwork}; + return network; } /** @@ -263,12 +264,12 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params fmt::arg(L"Ipams", fmt::join(params.ipams, L",")), fmt::arg(L"Policies", fmt::join(params.policies, L","))); - HCN_NETWORK network{nullptr}; + UniqueHcnNetwork network{}; const auto result = perform_hcn_operation(api, api.CreateNetwork, guid_from_string(params.guid), network_settings.c_str(), - &network); + util::out_ptr(network, api.CloseNetwork)); if (!result) { @@ -279,7 +280,6 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params static_cast(result.code)); } - [[maybe_unused]] UniqueHcnNetwork _{network, api.CloseNetwork}; return result; } @@ -324,14 +324,13 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para fmt::arg(L"HostComputeNetwork", maybe_widen{params.network_guid}), fmt::arg(L"MacAddress", params.mac_address ? fmt::format(L"\"{}\"", maybe_widen{params.mac_address.value()}) : L"null")); - HCN_ENDPOINT endpoint{nullptr}; + UniqueHcnEndpoint endpoint{}; const auto result = perform_hcn_operation(api, api.CreateEndpoint, network.get(), guid_from_string(params.endpoint_guid), endpoint_settings.c_str(), - &endpoint); - [[maybe_unused]] UniqueHcnEndpoint _{endpoint, api.CloseEndpoint}; + util::out_ptr(endpoint, api.CloseEndpoint)); return result; } From 4c503b2c63e9786c236a1a67ce6178692339398f Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 9 May 2025 16:52:59 +0300 Subject: [PATCH 65/76] [out_ptr] switch to P1132 reference implementation - update one more location that has been missed --- CMakeLists.txt | 2 + src/cmake/cmake-deps.cmake | 24 ++++ .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp | 16 +-- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 14 ++- .../backends/hyperv_api/util/out_ptr.h | 113 ------------------ 6 files changed, 43 insertions(+), 127 deletions(-) create mode 100644 src/cmake/cmake-deps.cmake delete mode 100644 src/platform/backends/hyperv_api/util/out_ptr.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3901c38c46..3eaf8896c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,6 +146,8 @@ if(UNIX) list(APPEND MULTIPASS_BACKENDS qemu) endif() +include(src/cmake/cmake-deps.cmake) + # OpenSSL config find_package(OpenSSL REQUIRED) diff --git a/src/cmake/cmake-deps.cmake b/src/cmake/cmake-deps.cmake new file mode 100644 index 0000000000..ec391923b7 --- /dev/null +++ b/src/cmake/cmake-deps.cmake @@ -0,0 +1,24 @@ +# 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(FetchContent) + +# Declare and fetch fmt +FetchContent_Declare( + out_ptr + GIT_REPOSITORY https://github.com/soasis/out_ptr.git + GIT_TAG 02a577edfcf25e2519e380a95c16743b7e5878a1 +) + +FetchContent_MakeAvailable(out_ptr) \ No newline at end of file diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index bed801d475..394f1c907a 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -60,6 +60,7 @@ if(WIN32) target_link_libraries(hyperv_api_backend PRIVATE fmt::fmt-header-only + ztd::out_ptr utils computecore.lib computenetwork.lib diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp index 22bc49d5f3..02c423caa7 100644 --- a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api_wrapper.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -36,11 +35,14 @@ // clang-format on #include +#include #include #include #include +using ztd::out_ptr::out_ptr; + namespace multipass::hyperv::hcn { @@ -156,7 +158,7 @@ OperationResult perform_hcn_operation(const HCNAPITable& api, const FnType& fn, // HcnClose*) is ErrorRecord, which is a JSON-formatted document emitted by // the API describing the error happened. Therefore, we can streamline all API // calls through perform_operation to perform co - const auto result = ResultCode{fn(std::forward(args)..., util::out_ptr(result_msgbuf, api.CoTaskMemFree))}; + const auto result = ResultCode{fn(std::forward(args)..., out_ptr(result_msgbuf, api.CoTaskMemFree))}; mpl::debug(kLogCategory, "perform_operation(...) > fn: {}, result: {}", @@ -187,10 +189,8 @@ UniqueHcnNetwork open_network(const HCNAPITable& api, const std::string& network mpl::debug(kLogCategory, "open_network(...) > network_guid: {} ", network_guid); UniqueHcnNetwork network{}; - const auto result = perform_hcn_operation(api, - api.OpenNetwork, - guid_from_string(network_guid), - util::out_ptr(network, api.CloseNetwork)); + const auto result = + perform_hcn_operation(api, api.OpenNetwork, guid_from_string(network_guid), out_ptr(network, api.CloseNetwork)); if (!result) { mpl::error(kLogCategory, "open_network() > HcnOpenNetwork failed with {}!", result.code); @@ -269,7 +269,7 @@ OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params api.CreateNetwork, guid_from_string(params.guid), network_settings.c_str(), - util::out_ptr(network, api.CloseNetwork)); + out_ptr(network, api.CloseNetwork)); if (!result) { @@ -330,7 +330,7 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para network.get(), guid_from_string(params.endpoint_guid), endpoint_settings.c_str(), - util::out_ptr(endpoint, api.CloseEndpoint)); + out_ptr(endpoint, api.CloseEndpoint)); return result; } diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index f9fe6bbe19..c3ff5a153b 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include @@ -33,6 +32,9 @@ #include #include +#include + +using ztd::out_ptr::out_ptr; namespace multipass::hyperv::hcs { @@ -91,9 +93,9 @@ OperationResult wait_for_operation_result(const HCSAPITable& api, fmt::ptr(op.get()), timeout.count()); - wchar_t* result_msg_out{nullptr}; - const auto hresult_code = ResultCode{api.WaitForOperationResult(op.get(), timeout.count(), &result_msg_out)}; - UniqueHlocalString result_msg{result_msg_out, api.LocalFree}; + UniqueHlocalString result_msg{}; + const auto hresult_code = + ResultCode{api.WaitForOperationResult(op.get(), timeout.count(), out_ptr(result_msg, api.LocalFree))}; mpl::debug(kLogCategory, "wait_for_operation_result(...) > finished ({}), result_code: {}", fmt::ptr(op.get()), @@ -128,7 +130,7 @@ UniqueHcsSystem open_host_compute_system(const HCSAPITable& api, const std::stri UniqueHcsSystem system{}; const ResultCode result = - api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, util::out_ptr(system, api.CloseComputeSystem)); + api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, out_ptr(system, api.CloseComputeSystem)); if (!result) { mpl::debug(kLogCategory, @@ -242,7 +244,7 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam vm_settings.c_str(), operation.get(), nullptr, - util::out_ptr(system, api.CloseComputeSystem))}; + out_ptr(system, api.CloseComputeSystem))}; if (!result) { diff --git a/src/platform/backends/hyperv_api/util/out_ptr.h b/src/platform/backends/hyperv_api/util/out_ptr.h deleted file mode 100644 index cd37148dd9..0000000000 --- a/src/platform/backends/hyperv_api/util/out_ptr.h +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 . - * - */ - -#ifndef MULTIPASS_HYPERV_API_UTIL_OUT_PTR_H -#define MULTIPASS_HYPERV_API_UTIL_OUT_PTR_H - -#include -#include - -namespace multipass::hyperv::util -{ - -namespace detail -{ -template -struct is_resettable : std::false_type -{ -}; - -template -struct is_resettable().reset(std::declval

(), std::declval()...))>, - A...> : std::true_type -{ -}; - -template -inline constexpr bool is_resettable_v = is_resettable::value; -} // namespace detail - -// A "just-enough" implementation of out_ptr -// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1132r5.html -template -class out_ptr_t -{ - Smart* sp_ = nullptr; // the smart pointer we’re updating - Pointer raw_ = nullptr; // where the C API will write the new handle - std::tuple extra_; // optional args forwarded to reset() - -public: - // Construction just remembers where to put things - out_ptr_t(Smart& s, Args&&... a) : sp_(std::addressof(s)), extra_(std::forward(a)...) - { - } - - // Non‑copyable, non‑movable – you get one shot per call site - out_ptr_t(const out_ptr_t&) = delete; - out_ptr_t& operator=(const out_ptr_t&) = delete; - out_ptr_t(out_ptr_t&&) = delete; - out_ptr_t& operator=(out_ptr_t&&) = delete; - - // The whole point: give the C API a place to stuff the pointer - operator Pointer*() const noexcept - { - return std::addressof(const_cast(raw_)); - } - - template ::type, void*>::value>> - operator void**() const noexcept - { - return reinterpret_cast(std::addressof(raw_)); - } - - // Cleanup: push the freshly‑filled pointer back into the smart pointer - ~out_ptr_t() - { - std::apply( - [&](auto&&... xs) { - if (sp_ && raw_) - { - if constexpr (detail::is_resettable_v) - { - sp_->reset(raw_, std::forward(xs)...); - } - else - { - *sp_ = Smart(raw_, std::forward(xs)...); - } - } - }, - extra_); - } -}; - -// Helper that lets you write std::out_ptr(up) with CTAD‑like deduction -template -[[nodiscard]] auto out_ptr(Smart& s, Args&&... a) -{ - using Ptr = typename Smart::pointer; - return out_ptr_t(s, std::forward(a)...); -} - -} // namespace multipass::hyperv::util - -#endif From a196e9c2c6878f95879764764a0d715a13fd3d55 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 14 May 2025 13:08:04 +0000 Subject: [PATCH 66/76] [hyperv-hcs] introduce modify_compute_system API and HcsRequest --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 10 ++ .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 14 ++ .../hyperv_api/hcs/hyperv_hcs_request.cpp | 78 +++++++++++ .../hyperv_api/hcs/hyperv_hcs_request.h | 55 ++++++++ .../hyperv_api/hcs/hyperv_hcs_request_type.h | 59 +++++++++ .../hyperv_api/hcs/hyperv_hcs_resource_path.h | 61 +++++++++ .../hcs/hyperv_hcs_wrapper_interface.h | 3 + tests/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/test_ut_hyperv_hcs_request.cpp | 121 ++++++++++++++++++ 10 files changed, 403 insertions(+) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h create mode 100644 tests/hyperv_api/test_ut_hyperv_hcs_request.cpp diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index 394f1c907a..f1eaab1686 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -51,6 +51,7 @@ if(WIN32) hcs/hyperv_hcs_network_adapter.cpp hcs/hyperv_hcs_plan9_share_params.cpp hcs/hyperv_hcs_create_compute_system_params.cpp + hcs/hyperv_hcs_request.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index c3ff5a153b..ec76f05e0c 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -542,4 +542,14 @@ OperationResult HCSWrapper::remove_plan9_share(const std::string& compute_system return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); } +// --------------------------------------------------------- + +OperationResult HCSWrapper::modify_compute_system(const std::string& compute_system_name, const HcsRequest& params) const +{ + mpl::debug(kLogCategory, "modify_compute_system(...) > params: {}", params); + + const auto json = fmt::to_wstring(params); + return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, json.c_str(), nullptr); +} + } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index d5cd63d461..ae5352177b 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -261,6 +261,20 @@ struct HCSWrapper : public HCSWrapperInterface [[nodiscard]] OperationResult remove_plan9_share(const std::string& compute_system_name, const Plan9ShareParameters& params) const override; + // --------------------------------------------------------- + + /** + * Modify a Host Compute System request. + * + * @param compute_system_name Target compute system name + * @param request The request + * + * @return An object that evaluates to true on success, false otherwise. + * message() may contain details of failure when result is false. + */ + [[nodiscard]] OperationResult modify_compute_system(const std::string& compute_system_name, + const HcsRequest& request) const override; + private: const HCSAPITable api{}; }; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp new file mode 100644 index 0000000000..246ce4837e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include + +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsNetworkAdapter; +using multipass::hyperv::hcs::HcsRequest; + +template +struct HcsRequestSettingsFormatters +{ + auto operator()(const std::monostate&) + { + constexpr auto null_str = MULTIPASS_UNIVERSAL_LITERAL("null"); + return std::basic_string{null_str.as()}; + } + + auto operator()(const HcsNetworkAdapter& params) + { + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "EndpointId": "{0}", + "MacAddress": "{1}", + "InstanceId": "{0}" + }} + )json"); + + return fmt::format(json_template.as(), + maybe_widen{params.endpoint_guid}, + maybe_widen{params.mac_address}); + } +}; + +template +template +auto fmt::formatter::format(const HcsRequest& param, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "ResourcePath": "{0}", + "RequestType": "{1}", + "Settings": {2} + }} + )json"); + + return format_to(ctx.out(), + json_template.as(), + maybe_widen{param.resource_path}, + maybe_widen{param.request_type}, + std::visit(HcsRequestSettingsFormatters{}, param.settings)); +} + +template auto fmt::formatter::format(const HcsRequest&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const HcsRequest&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h new file mode 100644 index 0000000000..90e34b7221 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -0,0 +1,55 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_REQUEST_H +#define MULTIPASS_HYPERV_API_HCS_REQUEST_H + +#include +#include +#include + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +/** + * @brief HcsRequest type for HCS modifications + */ +struct HcsRequest +{ + HcsResourcePath resource_path; + HcsRequestType request_type; + std::variant settings; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for HcsRequest + */ +template +struct fmt::formatter : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsRequest& param, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h new file mode 100644 index 0000000000..1757fc7dce --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h @@ -0,0 +1,59 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_REQUEST_TYPE_H +#define MULTIPASS_HYPERV_API_HCS_REQUEST_TYPE_H + +#include +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsRequestType +{ + operator std::string_view() const + { + return value; + } + + operator std::string() const + { + return std::string{value}; + } + + constexpr static auto Add() + { + return HcsRequestType{"Add"}; + } + + constexpr static auto Remove() + { + return HcsRequestType{"Remove"}; + } + +private: + constexpr HcsRequestType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcs + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h new file mode 100644 index 0000000000..0dc61c6b29 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h @@ -0,0 +1,61 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_RESOURCE_PATH_H +#define MULTIPASS_HYPERV_API_HCS_RESOURCE_PATH_H + +#include + +#include +#include +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsResourcePath +{ + operator std::string_view() const + { + return value; + } + + operator const std::string&() const + { + return value; + } + + static HcsResourcePath NetworkAdapters(const std::string& network_adapter_id) + { + std::filesystem::path result{network_adapters / fmt::format("{{{0}}}", network_adapter_id)}; + return HcsResourcePath(result.generic_string()); + } + +private: + HcsResourcePath(std::string v) : value(v) + { + } + + std::string value{}; + inline static const std::filesystem::path root{"VirtualMachine"}; + inline static const std::filesystem::path devices{root / "Devices"}; + inline static const std::filesystem::path network_adapters{devices / "NetworkAdapters"}; +}; + +} // namespace multipass::hyperv::hcs + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index cd356eb112..3809bfec5b 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -60,6 +61,8 @@ struct HCSWrapperInterface const Plan9ShareParameters& params) const = 0; virtual OperationResult remove_plan9_share(const std::string& compute_system_name, const Plan9ShareParameters& params) const = 0; + virtual OperationResult modify_compute_system(const std::string& compute_system_name, + const HcsRequest& request) const = 0; virtual ~HCSWrapperInterface() = default; }; } // namespace multipass::hyperv::hcs diff --git a/tests/hyperv_api/CMakeLists.txt b/tests/hyperv_api/CMakeLists.txt index bf350964cc..99e41c0b5b 100644 --- a/tests/hyperv_api/CMakeLists.txt +++ b/tests/hyperv_api/CMakeLists.txt @@ -25,6 +25,7 @@ if(WIN32) ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcn_network_policy.cpp ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_hcs_api.cpp ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_api.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_request.cpp ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_virtdisk.cpp ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_virtdisk.cpp ${CMAKE_CURRENT_LIST_DIR}/test_bb_cit_hyperv.cpp diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_request.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_request.cpp new file mode 100644 index 0000000000..1e22d0567b --- /dev/null +++ b/tests/hyperv_api/test_ut_hyperv_hcs_request.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "tests/common.h" +#include "tests/hyperv_api/hyperv_test_utils.h" + +#include +#include + +using multipass::hyperv::universal_string_literal_helper; +using multipass::hyperv::hcs::HcsNetworkAdapter; +using multipass::hyperv::hcs::HcsRequest; +using multipass::hyperv::hcs::HcsRequestType; +using multipass::hyperv::hcs::HcsResourcePath; + +namespace multipass::test +{ + +using uut_t = HcsRequest; + +template +struct HyperVHcsRequest_UnitTests : public ::testing::Test +{ + + template + static std::basic_string to_string(const T& v) + { + if constexpr (std::is_same_v) + { + return fmt::to_string(v); + } + else if constexpr (std::is_same_v) + { + return fmt::to_wstring(v); + } + } + + void do_test(const uut_t& uut, const universal_string_literal_helper& expected) + { + const auto result = to_string(uut); + const std::basic_string v{expected.as()}; + const auto result_nws = trim_whitespace(result.c_str()); + const auto expected_nws = trim_whitespace(v.c_str()); + EXPECT_EQ(result_nws, expected_nws); + } +}; + +using CharTypes = ::testing::Types; +TYPED_TEST_SUITE(HyperVHcsRequest_UnitTests, CharTypes); + +// --------------------------------------------------------- + +TYPED_TEST(HyperVHcsRequest_UnitTests, network_adapter_add_no_settings) +{ + uut_t uut{HcsResourcePath::NetworkAdapters("1111-2222-3333"), HcsRequestType::Add()}; + + constexpr auto expected_result = MULTIPASS_UNIVERSAL_LITERAL(R"json( + { + "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{1111-2222-3333}", + "RequestType": "Add", + "Settings": null + })json"); + + TestFixture::do_test(uut, expected_result); +} + +// --------------------------------------------------------- + +TYPED_TEST(HyperVHcsRequest_UnitTests, network_adapter_remove) +{ + uut_t uut{HcsResourcePath::NetworkAdapters("1111-2222-3333"), HcsRequestType::Remove()}; + + constexpr auto expected_result = MULTIPASS_UNIVERSAL_LITERAL(R"json( + { + "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{1111-2222-3333}", + "RequestType": "Remove", + "Settings": null + })json"); + + TestFixture::do_test(uut, expected_result); +} + +// --------------------------------------------------------- + +TYPED_TEST(HyperVHcsRequest_UnitTests, network_adapter_add_with_settings) +{ + uut_t uut{HcsResourcePath::NetworkAdapters("1111-2222-3333"), HcsRequestType::Add()}; + hyperv::hcs::HcsNetworkAdapter settings{}; + settings.endpoint_guid = "endpoint guid"; + settings.mac_address = "mac address"; + settings.instance_guid = "instance guid"; + uut.settings = settings; + constexpr auto expected_result = MULTIPASS_UNIVERSAL_LITERAL(R"json( + { + "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{1111-2222-3333}", + "RequestType": "Add", + "Settings": { + "EndpointId": "endpoint guid", + "MacAddress": "mac address", + "InstanceId": "endpoint guid" + } + })json"); + + TestFixture::do_test(uut, expected_result); +} + +} // namespace multipass::test From 1c410f538c60c2faab0db4e53125b68e8e029714 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 14 May 2025 14:31:39 +0000 Subject: [PATCH 67/76] [hyperv-hcs] replace add/remove_network_adapter with modify_cs modify_compute_system is the generic function for doing runtime CS modification. --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 47 +--------- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 32 ------- .../hcs/hyperv_hcs_wrapper_interface.h | 4 - tests/hyperv_api/test_bb_cit_hyperv.cpp | 13 ++- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 88 +++++++++++++------ 5 files changed, 76 insertions(+), 108 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index ec76f05e0c..45e1d0b300 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -310,50 +310,6 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::add_network_adapter(const std::string& compute_system_name, - const HcsNetworkAdapter& params) const -{ - mpl::debug(kLogCategory, "add_network_adapter(...) > params: {}", params); - constexpr auto settings_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", - "RequestType": "Add", - "Settings": {{ - "EndpointId": "{0}", - "MacAddress": "{1}", - "InstanceId": "{0}" - }} - }})"; - - const auto settings = - fmt::format(settings_template, maybe_widen{params.endpoint_guid}, maybe_widen{params.mac_address}); - - return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); -} - -// --------------------------------------------------------- - -OperationResult HCSWrapper::remove_network_adapter(const std::string& compute_system_name, - const std::string& endpoint_guid) const -{ - mpl::debug(kLogCategory, - "remove_network_adapter(...) > name: ({}), endpoint_guid: ({})", - compute_system_name, - endpoint_guid); - - constexpr auto settings_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", - "RequestType": "Remove" - }})"; - - const auto settings = fmt::format(settings_template, maybe_widen{endpoint_guid}); - - return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); -} - -// --------------------------------------------------------- - OperationResult HCSWrapper::resize_memory(const std::string& compute_system_name, std::uint32_t new_size_mib) const { // Machine must be booted up. @@ -544,7 +500,8 @@ OperationResult HCSWrapper::remove_plan9_share(const std::string& compute_system // --------------------------------------------------------- -OperationResult HCSWrapper::modify_compute_system(const std::string& compute_system_name, const HcsRequest& params) const +OperationResult HCSWrapper::modify_compute_system(const std::string& compute_system_name, + const HcsRequest& params) const { mpl::debug(kLogCategory, "modify_compute_system(...) > params: {}", params); diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index ae5352177b..2576c2550f 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -160,38 +160,6 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- - /** - * Add a network adapter to the host compute system. - * - * A new network interface card will be automatically created for - * the endpoint. The network interface card's name will be the - * endpoint's GUID for convenience. - * - * @param [in] compute_system_name Target compute system's name - * @param [in] params Network adapter parameters - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult add_network_adapter(const std::string& compute_system_name, - const HcsNetworkAdapter& params) const override; - - // --------------------------------------------------------- - - /** - * Remove a network adapter from the host compute system. - * - * @param [in] name Target compute system's name - * @param [in] endpoint_guid GUID of the endpoint to remove - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult remove_network_adapter(const std::string& compute_system_name, - const std::string& endpoint_guid) const override; - - // --------------------------------------------------------- - /** * Resize the amount of memory the compute system has. * diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index 3809bfec5b..f3b7efdd44 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -47,10 +47,6 @@ struct HCSWrapperInterface const std::filesystem::path& file_path) const = 0; virtual OperationResult revoke_vm_access(const std::string& compute_system_name, const std::filesystem::path& file_path) const = 0; - virtual OperationResult add_network_adapter(const std::string& compute_system_name, - const HcsNetworkAdapter& params) const = 0; - virtual OperationResult remove_network_adapter(const std::string& compute_system_name, - const std::string& endpoint_guid) const = 0; virtual OperationResult resize_memory(const std::string& compute_system_name, const std::uint32_t new_size_mib) const = 0; virtual OperationResult update_cpu_count(const std::string& compute_system_name, diff --git a/tests/hyperv_api/test_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index 25741a7104..1114961504 100644 --- a/tests/hyperv_api/test_bb_cit_hyperv.cpp +++ b/tests/hyperv_api/test_bb_cit_hyperv.cpp @@ -33,6 +33,11 @@ using hcn_wrapper_t = hyperv::hcn::HCNWrapper; using hcs_wrapper_t = hyperv::hcs::HCSWrapper; using virtdisk_wrapper_t = multipass::hyperv::virtdisk::VirtDiskWrapper; +using multipass::hyperv::hcs::HcsNetworkAdapter; +using multipass::hyperv::hcs::HcsRequest; +using multipass::hyperv::hcs::HcsRequestType; +using multipass::hyperv::hcs::HcsResourcePath; + // 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. struct HyperV_ComponentIntegrationTests : public ::testing::Test @@ -225,9 +230,13 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_bo ASSERT_TRUE(status_msg.empty()); } - // Add endpoint + // Add network adapter { - const auto& [status, status_msg] = hcs.add_network_adapter(create_vm_parameters.name, network_adapter); + const HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(network_adapter.endpoint_guid), + HcsRequestType::Add(), + network_adapter}; + const auto& [status, status_msg] = + hcs.modify_compute_system(create_vm_parameters.name, add_network_adapter_req); ASSERT_TRUE(status); ASSERT_TRUE(status_msg.empty()); } diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 5933debb90..7a10fa154d 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -37,6 +37,11 @@ namespace multipass::test { using uut_t = hyperv::hcs::HCSWrapper; +using hyperv::hcs::HcsNetworkAdapter; +using hyperv::hcs::HcsRequest; +using hyperv::hcs::HcsRequestType; +using hyperv::hcs::HcsResourcePath; + struct HyperVHCSAPI_UnitTests : public ::testing::Test { mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); @@ -2257,11 +2262,15 @@ TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_happy_path) generic_operation_happy_path( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...) > params:"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_network_adapter("test_vm", params); + + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return wrapper.modify_compute_system("test_vm", add_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2279,9 +2288,12 @@ TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_hcs_open_fa generic_operation_hcs_open_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); hyperv::hcs::HcsNetworkAdapter params{}; - return wrapper.add_network_adapter("test_vm", params); + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return wrapper.modify_compute_system("test_vm", add_network_adapter_req); }); } @@ -2292,9 +2304,12 @@ TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_create_oper generic_operation_create_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); hyperv::hcs::HcsNetworkAdapter params{}; - return wrapper.add_network_adapter("test_vm", params); + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return wrapper.modify_compute_system("test_vm", add_network_adapter_req); }); } @@ -2316,11 +2331,14 @@ TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_fail) generic_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_network_adapter("test_vm", params); + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return wrapper.modify_compute_system("test_vm", add_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2349,11 +2367,14 @@ TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_wait_for_op generic_operation_wait_for_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "add_network_adapter(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); hyperv::hcs::HcsNetworkAdapter params{}; params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; params.mac_address = "00:00:00:00:00:00"; - return wrapper.add_network_adapter("test_vm", params); + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return wrapper.modify_compute_system("test_vm", add_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2371,16 +2392,19 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_happy_ constexpr auto expected_modify_compute_system_configuration = LR"( { "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{288cc1ac-8f31-4a09-9e90-30ad0bcfdbca}", - "RequestType": "Remove" + "RequestType": "Remove", + "Settings": null })"; generic_operation_happy_path( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log( - mpl::Level::debug, - "remove_network_adapter(...) > name: (test_vm), endpoint_guid: (288cc1ac-8f31-4a09-9e90-30ad0bcfdbca)"); - return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); + + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return wrapper.modify_compute_system("test_vm", remove_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2398,8 +2422,11 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_hcs_op generic_operation_hcs_open_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); - return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return wrapper.modify_compute_system("test_vm", remove_network_adapter_req); }); } @@ -2410,8 +2437,11 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_create generic_operation_create_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); - return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return wrapper.modify_compute_system("test_vm", remove_network_adapter_req); }); } @@ -2422,14 +2452,18 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_fail) constexpr auto expected_modify_compute_system_configuration = LR"( { "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{288cc1ac-8f31-4a09-9e90-30ad0bcfdbca}", - "RequestType": "Remove" + "RequestType": "Remove", + "Settings": null })"; generic_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); - return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return wrapper.modify_compute_system("test_vm", remove_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2447,14 +2481,18 @@ TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_wait_f constexpr auto expected_modify_compute_system_configuration = LR"( { "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{288cc1ac-8f31-4a09-9e90-30ad0bcfdbca}", - "RequestType": "Remove" + "RequestType": "Remove", + "Settings": null })"; generic_operation_wait_for_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "remove_network_adapter(...)"); - return wrapper.remove_network_adapter("test_vm", "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params:"); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return wrapper.modify_compute_system("test_vm", remove_network_adapter_req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); From 5707e3a3d77d3527992597d30bbafaabe66f7c47 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 15 May 2025 14:39:31 +0000 Subject: [PATCH 68/76] [hyperv-hcs] replace resize_memory with modify_compute_system --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 19 ---------- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 14 ------- .../hcs/hyperv_hcs_modify_memory_settings.h | 35 ++++++++++++++++++ .../hyperv_api/hcs/hyperv_hcs_request.cpp | 10 +++++ .../hyperv_api/hcs/hyperv_hcs_request.h | 3 +- .../hyperv_api/hcs/hyperv_hcs_request_type.h | 4 ++ .../hyperv_api/hcs/hyperv_hcs_resource_path.h | 12 +++--- .../hcs/hyperv_hcs_wrapper_interface.h | 2 - .../hyperv_api/hcs_virtual_machine.cpp | 9 ++++- .../hyperv_api/hyperv_api_string_conversion.h | 2 +- tests/hyperv_api/test_ut_hyperv_hcs_api.cpp | 37 +++++++++++++------ 11 files changed, 91 insertions(+), 56 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_modify_memory_settings.h diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 45e1d0b300..4d4ef73bed 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -310,25 +310,6 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::resize_memory(const std::string& compute_system_name, std::uint32_t new_size_mib) const -{ - // Machine must be booted up. - mpl::debug(kLogCategory, "resize_memory(...) > name: ({}), new_size_mb: ({})", compute_system_name, new_size_mib); - // https://learn.microsoft.com/en-us/virtualization/api/hcs/reference/hcsmodifycomputesystem#remarks - constexpr auto resize_memory_settings_template = LR"( - {{ - "ResourcePath": "VirtualMachine/ComputeTopology/Memory/SizeInMB", - "RequestType": "Update", - "Settings": {0} - }})"; - - const auto settings = fmt::format(resize_memory_settings_template, new_size_mib); - - return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); -} - -// --------------------------------------------------------- - OperationResult HCSWrapper::update_cpu_count(const std::string& compute_system_name, std::uint32_t new_vcpu_count) const { return OperationResult{E_NOTIMPL, L"Not implemented yet!"}; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index 2576c2550f..bce5583085 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -160,20 +160,6 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- - /** - * Resize the amount of memory the compute system has. - * - * @param compute_system_name Target compute system name - * @param new_size_mib New memory size, in megabytes - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult resize_memory(const std::string& compute_system_name, - std::uint32_t new_size_mib) const override; - - // --------------------------------------------------------- - /** * Change the amount of available vCPUs in the compute system * diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_modify_memory_settings.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_modify_memory_settings.h new file mode 100644 index 0000000000..239c0b33c2 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_modify_memory_settings.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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_MODIFY_MEMORY_SETTINGS_H +#define MULTIPASS_HYPERV_API_HCS_MODIFY_MEMORY_SETTINGS_H + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsModifyMemorySettings +{ + std::uint32_t size_in_mb{0}; +}; + +} // namespace multipass::hyperv::hcs + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp index 246ce4837e..8b8ed45b1d 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp @@ -21,6 +21,7 @@ #include using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsModifyMemorySettings; using multipass::hyperv::hcs::HcsNetworkAdapter; using multipass::hyperv::hcs::HcsRequest; @@ -47,6 +48,15 @@ struct HcsRequestSettingsFormatters maybe_widen{params.endpoint_guid}, maybe_widen{params.mac_address}); } + + auto operator()(const HcsModifyMemorySettings& params) + { + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {0} + )json"); + + return fmt::format(json_template.as(), params.size_in_mb); + } }; template diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h index 90e34b7221..2a6d198ca3 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -18,6 +18,7 @@ #ifndef MULTIPASS_HYPERV_API_HCS_REQUEST_H #define MULTIPASS_HYPERV_API_HCS_REQUEST_H +#include #include #include #include @@ -36,7 +37,7 @@ struct HcsRequest { HcsResourcePath resource_path; HcsRequestType request_type; - std::variant settings; + std::variant settings{std::monostate{}}; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h index 1757fc7dce..e36e0ba65c 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h @@ -46,6 +46,10 @@ struct HcsRequestType return HcsRequestType{"Remove"}; } + constexpr static auto Update(){ + return HcsRequestType{"Update"}; + } + private: constexpr HcsRequestType(std::string_view v) : value(v) { diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h index 0dc61c6b29..1aaaff33a4 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h @@ -20,7 +20,6 @@ #include -#include #include #include @@ -41,8 +40,12 @@ struct HcsResourcePath static HcsResourcePath NetworkAdapters(const std::string& network_adapter_id) { - std::filesystem::path result{network_adapters / fmt::format("{{{0}}}", network_adapter_id)}; - return HcsResourcePath(result.generic_string()); + return fmt::format("VirtualMachine/Devices/NetworkAdapters/{{{0}}}", network_adapter_id); + } + + static HcsResourcePath Memory() + { + return std::string{"VirtualMachine/ComputeTopology/Memory/SizeInMB"}; } private: @@ -51,9 +54,6 @@ struct HcsResourcePath } std::string value{}; - inline static const std::filesystem::path root{"VirtualMachine"}; - inline static const std::filesystem::path devices{root / "Devices"}; - inline static const std::filesystem::path network_adapters{devices / "NetworkAdapters"}; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index f3b7efdd44..88ce678eca 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -47,8 +47,6 @@ struct HCSWrapperInterface const std::filesystem::path& file_path) const = 0; virtual OperationResult revoke_vm_access(const std::string& compute_system_name, const std::filesystem::path& file_path) const = 0; - virtual OperationResult resize_memory(const std::string& compute_system_name, - const std::uint32_t new_size_mib) const = 0; virtual OperationResult update_cpu_count(const std::string& compute_system_name, const std::uint32_t new_core_count) const = 0; virtual OperationResult get_compute_system_state(const std::string& compute_system_name, diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 3ffc42a5c6..ddf7b01f20 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -520,15 +520,20 @@ void HCSVirtualMachine::update_cpus(int num_cores) throw std::runtime_error{"Not yet implemented"}; } + void HCSVirtualMachine::resize_memory(const MemorySize& new_size) { mpl::debug(kLogCategory, "resize_memory() -> called for VM `{}`, new_size `{}` MiB", vm_name, new_size.in_megabytes()); - - const auto& [status, status_msg] = hcs->resize_memory(vm_name, new_size.in_megabytes()); + hcs::HcsRequest req{hcs::HcsResourcePath::Memory(), + hcs::HcsRequestType::Update(), + hcs::HcsModifyMemorySettings{static_cast(new_size.in_megabytes())}}; + hcs->modify_compute_system(vm_name, req); + // FIXME: Log the result. } + void HCSVirtualMachine::resize_disk(const MemorySize& new_size) { mpl::debug(kLogCategory, diff --git a/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h index f02604b21c..3572fa6835 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h +++ b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h @@ -97,4 +97,4 @@ struct fmt::formatter "" X, L"" X \ } -#endif // MULTIPASS_HYPERV_API_STRING_CONVERSION_H +#endif // MULTIPASS_HYPERV_API_STRING_CONVERSION_H \ No newline at end of file diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 7a10fa154d..b971d4f0cd 100644 --- a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -2517,9 +2517,12 @@ TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_happy_path) generic_operation_happy_path( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, - "resize_memory(...) > name: (test_vm), new_size_mb: (16384)"); - return wrapper.resize_memory("test_vm", 16384); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...) > params"); + hyperv::hcs::HcsRequest req{hyperv::hcs::HcsResourcePath::Memory(), + hyperv::hcs::HcsRequestType::Update(), + hyperv::hcs::HcsModifyMemorySettings{16384}}; + + return wrapper.modify_compute_system("test_vm", req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2537,8 +2540,11 @@ TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_hcs_open_fail) generic_operation_hcs_open_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "resize_memory(...)"); - return wrapper.resize_memory("test_vm", 16384); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsRequest req{hyperv::hcs::HcsResourcePath::Memory(), + hyperv::hcs::HcsRequestType::Update(), + hyperv::hcs::HcsModifyMemorySettings{16384}}; + return wrapper.modify_compute_system("test_vm", req); }); } @@ -2549,8 +2555,11 @@ TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_create_operation_ generic_operation_create_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "resize_memory(...)"); - return wrapper.resize_memory("test_vm", 16384); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsRequest req{hyperv::hcs::HcsResourcePath::Memory(), + hyperv::hcs::HcsRequestType::Update(), + hyperv::hcs::HcsModifyMemorySettings{16384}}; + return wrapper.modify_compute_system("test_vm", req); }); } @@ -2568,8 +2577,11 @@ TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_fail) generic_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "resize_memory(...)"); - return wrapper.resize_memory("test_vm", 16384); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsRequest req{hyperv::hcs::HcsResourcePath::Memory(), + hyperv::hcs::HcsRequestType::Update(), + hyperv::hcs::HcsModifyMemorySettings{16384}}; + return wrapper.modify_compute_system("test_vm", req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); @@ -2594,8 +2606,11 @@ TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_wait_for_operatio generic_operation_wait_for_operation_fail( mock_api_table.ModifyComputeSystem, [&](hyperv::hcs::HCSWrapper& wrapper) { - logger_scope.mock_logger->expect_log(mpl::Level::debug, "resize_memory(...)"); - return wrapper.resize_memory("test_vm", 16384); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsRequest req{hyperv::hcs::HcsResourcePath::Memory(), + hyperv::hcs::HcsRequestType::Update(), + hyperv::hcs::HcsModifyMemorySettings{16384}}; + return wrapper.modify_compute_system("test_vm", req); }, [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity) { ASSERT_EQ(mock_compute_system_object, computeSystem); From 28ea377811442b6f58931f5e4ce401ab03165c08 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Thu, 15 May 2025 14:52:04 +0000 Subject: [PATCH 69/76] [lint] make the linter happy --- src/cmake/cmake-deps.cmake | 2 +- src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h | 3 ++- .../backends/hyperv_api/hyperv_api_string_conversion.h | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cmake/cmake-deps.cmake b/src/cmake/cmake-deps.cmake index ec391923b7..944bf22244 100644 --- a/src/cmake/cmake-deps.cmake +++ b/src/cmake/cmake-deps.cmake @@ -21,4 +21,4 @@ FetchContent_Declare( GIT_TAG 02a577edfcf25e2519e380a95c16743b7e5878a1 ) -FetchContent_MakeAvailable(out_ptr) \ No newline at end of file +FetchContent_MakeAvailable(out_ptr) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h index e36e0ba65c..ec4660e89c 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h @@ -46,7 +46,8 @@ struct HcsRequestType return HcsRequestType{"Remove"}; } - constexpr static auto Update(){ + constexpr static auto Update() + { return HcsRequestType{"Update"}; } diff --git a/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h index 3572fa6835..f02604b21c 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h +++ b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h @@ -97,4 +97,4 @@ struct fmt::formatter "" X, L"" X \ } -#endif // MULTIPASS_HYPERV_API_STRING_CONVERSION_H \ No newline at end of file +#endif // MULTIPASS_HYPERV_API_STRING_CONVERSION_H From 80c1d26bdb3d870be3b7f0d9625182734870dfde Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 16 May 2025 08:33:24 +0000 Subject: [PATCH 70/76] [hyperv-hcs] replace 9p API functions with modify_compute_system --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 83 ------------------- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 29 ------- .../hyperv_hcs_create_compute_system_params.h | 2 +- .../hcs/hyperv_hcs_plan9_share_params.cpp | 53 +++++++++--- .../hcs/hyperv_hcs_plan9_share_params.h | 42 +++++++--- .../hyperv_api/hcs/hyperv_hcs_request.cpp | 30 ++++++- .../hyperv_api/hcs/hyperv_hcs_request.h | 9 +- .../hyperv_api/hcs/hyperv_hcs_resource_path.h | 5 ++ .../hcs/hyperv_hcs_wrapper_interface.h | 4 - .../hyperv_api/hcs_plan9_mount_handler.cpp | 37 ++++++--- tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 28 +++++-- 11 files changed, 157 insertions(+), 165 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 4d4ef73bed..2952d4da28 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -398,89 +398,6 @@ OperationResult HCSWrapper::get_compute_system_state(const std::string& compute_ // --------------------------------------------------------- -OperationResult HCSWrapper::add_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const -{ - - mpl::debug(kLogCategory, "add_plan9_share(...) > name: ({}), params({})", compute_system_name, params); - // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/plan9.go#L13 - // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#System_PropertyType - // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/hcs/schema2/plan9_share.go#L12 - - // Settings: hcsschema.Plan9Share{ - // Name: name, - // AccessName: name, - // Path: path, - // Port: port, - // Flags: flags, - // AllowedFiles: allowed, // < this one is not supported in the base API - // }, - // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/builder.go#L53 - constexpr auto add_plan9_share_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/Plan9/Shares", - "RequestType": "Add", - "Settings": {{ - "Name": "{0}", - "Path": "{1}", - "Port": {2}, - "AccessName": "{3}", - "Flags": 33 - }} - }})"; - - auto preferred{params.host_path}; - preferred.make_preferred(); - - (void)grant_vm_access(compute_system_name, params.host_path); - - const auto settings = fmt::format(add_plan9_share_template, - maybe_widen{params.name}, - // generic_* always uses / as separator, use it - // to normalize the path. HCS API does not like - // `\` for example. - preferred.generic_wstring(), - params.port, - maybe_widen{params.access_name}, - fmt::underlying(params.flags)); - fmt::print(L"{}", settings); - return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); -} - -// --------------------------------------------------------- - -OperationResult HCSWrapper::remove_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const -{ - mpl::debug(kLogCategory, "remove_plan9_share(...) > name: ({}), params({})", compute_system_name, params); - // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/plan9.go#L29 - - // Settings: hcsschema.Plan9Share{ - // Name: name, - // AccessName: name, - // Port: port, - // }, - - constexpr auto remove_plan9_share_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/Plan9/Shares", - "RequestType": "Remove", - "Settings": {{ - "Name": "{0}", - "AccessName": "{1}", - "Port": {2} - }} - }})"; - - const auto settings = fmt::format(remove_plan9_share_template, - maybe_widen{params.name}, - maybe_widen{params.access_name}, - params.port); - return perform_hcs_operation(api, api.ModifyComputeSystem, compute_system_name, settings.c_str(), nullptr); -} - -// --------------------------------------------------------- - OperationResult HCSWrapper::modify_compute_system(const std::string& compute_system_name, const HcsRequest& params) const { diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index bce5583085..14cd8bc5ab 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -188,35 +188,6 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- - /** - * Add a Plan9 share to a running system - * - * @param [in] compute_system_name Target compute system's name - * @param [in] params Plan9 share details - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult add_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const override; - - // --------------------------------------------------------- - - /** - * Remove a Plan9 share from a running system - * - * @param [in] compute_system_name Target compute system's name - * @param [in] params Plan9 share to remove. It's sufficient to fill the - * name, access_name and port. The rest are redundant for remove. - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult remove_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const override; - - // --------------------------------------------------------- - /** * Modify a Host Compute System request. * diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h index 2ebaaf9392..2eec847d20 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -64,7 +64,7 @@ struct CreateComputeSystemParameters * List of Plan9 shares that'll be added to the compute system * by default at creation time. */ - std::vector shares{}; + std::vector shares{}; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp index 5749b2e34e..c05cac9756 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp @@ -21,11 +21,13 @@ #include using multipass::hyperv::maybe_widen; -using multipass::hyperv::hcs::Plan9ShareParameters; +using multipass::hyperv::hcs::HcsAddPlan9ShareParameters; +using multipass::hyperv::hcs::HcsRemovePlan9ShareParameters; template template -auto fmt::formatter::format(const Plan9ShareParameters& params, FormatContext& ctx) const -> +auto fmt::formatter::format(const HcsAddPlan9ShareParameters& params, + FormatContext& ctx) const -> typename FormatContext::iterator { constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( @@ -34,23 +36,52 @@ auto fmt::formatter::format(const Plan9ShareParamete "Path": "{1}", "Port": {2}, "AccessName": "{3}", - "Flags": "{4}" + "Flags": {4} }} )json"); - + const auto path_str = params.host_path.generic_string(); return format_to(ctx.out(), json_template.as(), maybe_widen{params.name}, - params.host_path, + maybe_widen{path_str}, params.port, maybe_widen{params.access_name}, fmt::underlying(params.flags)); } -template auto fmt::formatter::format(const Plan9ShareParameters&, - fmt::format_context&) const - -> fmt::format_context::iterator; +template +template +auto fmt::formatter::format(const HcsRemovePlan9ShareParameters& params, + FormatContext& ctx) const -> + typename FormatContext::iterator +{ + constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( + {{ + "Name": "{0}", + "AccessName": "{1}", + "Port": {2} + }} + )json"); + + return format_to(ctx.out(), + json_template.as(), + maybe_widen{params.name}, + maybe_widen{params.access_name}, + params.port); +} + +template auto fmt::formatter::format( + const HcsAddPlan9ShareParameters&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const HcsAddPlan9ShareParameters&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; + +template auto fmt::formatter::format( + const HcsRemovePlan9ShareParameters&, + fmt::format_context&) const -> fmt::format_context::iterator; -template auto fmt::formatter::format(const Plan9ShareParameters&, - fmt::wformat_context&) const - -> fmt::wformat_context::iterator; +template auto fmt::formatter::format( + const HcsRemovePlan9ShareParameters&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h index 7ba2d30e82..bbe32770e9 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -33,10 +33,9 @@ enum class Plan9ShareFlags : std::uint32_t case_sensitive = 0x00000008 }; -/** - * Parameters for creating a Plan9 share. - */ -struct Plan9ShareParameters +namespace detail +{ +struct HcsPlan9Base { /** * The default port number for Plan9. @@ -45,7 +44,6 @@ struct Plan9ShareParameters * since the host might want to run a Plan9 server itself. */ static inline constexpr std::uint16_t default_port{55035}; - /** * Unique name for the share */ @@ -58,14 +56,22 @@ struct Plan9ShareParameters std::string access_name{}; /** - * Host directory to share + * Target port. */ - std::filesystem::path host_path{}; + std::uint16_t port{default_port}; +}; +} // namespace detail + +struct HcsRemovePlan9ShareParameters : public detail::HcsPlan9Base +{ +}; +struct HcsAddPlan9ShareParameters : public detail::HcsPlan9Base +{ /** - * Target path. + * Host directory to share */ - std::uint16_t port{default_port}; + std::filesystem::path host_path{}; /** * ReadOnly 0x00000001 @@ -74,17 +80,29 @@ struct Plan9ShareParameters */ Plan9ShareFlags flags{Plan9ShareFlags::none}; }; - } // namespace multipass::hyperv::hcs /** * Formatter type specialization for Plan9ShareParameters */ template -struct fmt::formatter : formatter, Char> +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsAddPlan9ShareParameters& param, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +/** + * Formatter type specialization for Plan9ShareParameters + */ +template +struct fmt::formatter + : formatter, Char> { template - auto format(const multipass::hyperv::hcs::Plan9ShareParameters& policy, FormatContext& ctx) const -> + auto format(const multipass::hyperv::hcs::HcsRemovePlan9ShareParameters& param, FormatContext& ctx) const -> typename FormatContext::iterator; }; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp index 8b8ed45b1d..8cdd8a4aec 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp @@ -21,13 +21,29 @@ #include using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsAddPlan9ShareParameters; using multipass::hyperv::hcs::HcsModifyMemorySettings; using multipass::hyperv::hcs::HcsNetworkAdapter; +using multipass::hyperv::hcs::HcsRemovePlan9ShareParameters; using multipass::hyperv::hcs::HcsRequest; template struct HcsRequestSettingsFormatters { + + template + static auto to_string(const T& v) + { + if constexpr (std::is_same_v) + { + return fmt::to_string(v); + } + else if constexpr (std::is_same_v) + { + return fmt::to_wstring(v); + } + } + auto operator()(const std::monostate&) { constexpr auto null_str = MULTIPASS_UNIVERSAL_LITERAL("null"); @@ -51,11 +67,17 @@ struct HcsRequestSettingsFormatters auto operator()(const HcsModifyMemorySettings& params) { - constexpr static auto json_template = MULTIPASS_UNIVERSAL_LITERAL(R"json( - {0} - )json"); + return to_string(params.size_in_mb); + } - return fmt::format(json_template.as(), params.size_in_mb); + auto operator()(const HcsAddPlan9ShareParameters& params) + { + return to_string(params); + } + + auto operator()(const HcsRemovePlan9ShareParameters& params) + { + return to_string(params); } }; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h index 2a6d198ca3..347f6d4db0 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -20,9 +20,11 @@ #include #include +#include #include #include + #include #include @@ -37,7 +39,12 @@ struct HcsRequest { HcsResourcePath resource_path; HcsRequestType request_type; - std::variant settings{std::monostate{}}; + std::variant + settings{std::monostate{}}; }; } // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h index 1aaaff33a4..6f4d3a3ab4 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h @@ -48,6 +48,11 @@ struct HcsResourcePath return std::string{"VirtualMachine/ComputeTopology/Memory/SizeInMB"}; } + static HcsResourcePath Plan9Shares() + { + return std::string{"VirtualMachine/Devices/Plan9/Shares"}; + } + private: HcsResourcePath(std::string v) : value(v) { diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index 88ce678eca..f943ce09a8 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -51,10 +51,6 @@ struct HCSWrapperInterface const std::uint32_t new_core_count) const = 0; virtual OperationResult get_compute_system_state(const std::string& compute_system_name, ComputeSystemState& state_out) const = 0; - virtual OperationResult add_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const = 0; - virtual OperationResult remove_plan9_share(const std::string& compute_system_name, - const Plan9ShareParameters& params) const = 0; virtual OperationResult modify_compute_system(const std::string& compute_system_name, const HcsRequest& request) const = 0; virtual ~HCSWrapperInterface() = default; diff --git a/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp index 56fafb4304..cc5725ea1f 100644 --- a/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp @@ -53,15 +53,19 @@ Plan9MountHandler::~Plan9MountHandler() = default; void Plan9MountHandler::activate_impl(ServerVariant server, std::chrono::milliseconds timeout) { - const auto params = [this] { - hcs::Plan9ShareParameters params{}; + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/plan9.go#L13 + // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#System_PropertyType + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/hcs/schema2/plan9_share.go#L12 + // https://github.com/microsoft/hcsshim/blob/d7e384230944f153215473fa6c715b8723d1ba47/internal/vm/hcs/builder.go#L53 + const auto req = [this] { + hcs::HcsAddPlan9ShareParameters params{}; params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); params.name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); params.host_path = mount_spec.get_source_path(); - return params; + return hcs::HcsRequest{hcs::HcsResourcePath::Plan9Shares(), hcs::HcsRequestType::Add(), params}; }(); - const auto result = hcs->add_plan9_share(vm->vm_name, params); + const auto result = hcs->modify_compute_system(vm->vm_name, req); if (!result) { @@ -94,13 +98,15 @@ void Plan9MountHandler::activate_impl(ServerVariant server, std::chrono::millise constexpr std::string_view mount_command_fmtstr = "sudo mount -t 9p -o trans=virtio,version=9p2000.L,port={} {} {}"; - const auto mount_command = fmt::format(mount_command_fmtstr, params.port, params.access_name, target); + const auto& add_settings = std::get(req.settings); + const auto mount_command = + fmt::format(mount_command_fmtstr, add_settings.port, add_settings.access_name, target); auto mount_command_result = session.exec(mount_command); if (mount_command_result.exit_code() == 0) { - mpl::info(kLogCategory, "Successfully mounted 9P share `{}` to VM `{}`", params, vm->vm_name); + mpl::info(kLogCategory, "Successfully mounted 9P share `{}` to VM `{}`", req, vm->vm_name); } else { @@ -113,7 +119,13 @@ void Plan9MountHandler::activate_impl(ServerVariant server, std::chrono::millise } catch (...) { - if (!hcs->remove_plan9_share(vm->vm_name, params)) + const auto req = [this] { + hcs::HcsRemovePlan9ShareParameters params{}; + params.name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + return hcs::HcsRequest{hcs::HcsResourcePath::Plan9Shares(), hcs::HcsRequestType::Remove(), params}; + }(); + if (!hcs->modify_compute_system(vm->vm_name, req)) { // TODO: Warn here } @@ -136,15 +148,14 @@ void Plan9MountHandler::deactivate_impl(bool force) } } - const auto params = [this] { - hcs::Plan9ShareParameters params{}; - params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + const auto req = [this] { + hcs::HcsRemovePlan9ShareParameters params{}; params.name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); - params.host_path = mount_spec.get_source_path(); - return params; + params.access_name = mpu::make_uuid(target).remove("-").left(30).prepend('m').toStdString(); + return hcs::HcsRequest{hcs::HcsResourcePath::Plan9Shares(), hcs::HcsRequestType::Remove(), params}; }(); - if (!hcs->remove_plan9_share(vm->vm_name, params)) + if (!hcs->modify_compute_system(vm->vm_name, req)) { mpl::warn(kLogCategory, "Plan9 share removal failed."); } diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 038d020a4e..34b4ebf76a 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -196,16 +196,30 @@ TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) EXPECT_TRUE(p_result); std::wprintf(L"%s\n", p_result.status_msg.c_str()); - hyperv::hcs::Plan9ShareParameters share{}; - share.access_name = "test"; - share.name = "test"; - share.host_path = "C://"; - - const auto sh_a_result = uut.add_plan9_share(params.name, share); + const auto add_9p_req = []() { + hyperv::hcs::HcsAddPlan9ShareParameters share{}; + share.access_name = "test"; + share.name = "test"; + share.host_path = "C://"; + return hyperv::hcs::HcsRequest{hyperv::hcs::HcsResourcePath::Plan9Shares(), + hyperv::hcs::HcsRequestType::Add(), + share}; + }(); + + const auto sh_a_result = uut.modify_compute_system(params.name, add_9p_req); EXPECT_TRUE(sh_a_result); std::wprintf(L"%s\n", sh_a_result.status_msg.c_str()); - const auto sh_r_result = uut.remove_plan9_share(params.name, share); + const auto remove_9p_req = []() { + hyperv::hcs::HcsRemovePlan9ShareParameters share{}; + share.access_name = "test"; + share.name = "test"; + return hyperv::hcs::HcsRequest{hyperv::hcs::HcsResourcePath::Plan9Shares(), + hyperv::hcs::HcsRequestType::Remove(), + share}; + }(); + + const auto sh_r_result = uut.modify_compute_system(params.name, remove_9p_req); EXPECT_TRUE(sh_r_result); std::wprintf(L"%s\n", sh_r_result.status_msg.c_str()); From db0dad67d660038ce382e5a91fb6d91dbb010b92 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 16 May 2025 08:58:14 +0000 Subject: [PATCH 71/76] [hyperv-hcs] remove update_cpu_count function --- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp | 7 ---- .../hyperv_api/hcs/hyperv_hcs_api_wrapper.h | 14 ------- .../hyperv_api/hcs/hyperv_hcs_request.h | 1 - .../hcs/hyperv_hcs_wrapper_interface.h | 2 - tests/hyperv_api/test_it_hyperv_hcs_api.cpp | 41 ------------------- 5 files changed, 65 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp index 2952d4da28..06443dbcee 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.cpp @@ -310,13 +310,6 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::update_cpu_count(const std::string& compute_system_name, std::uint32_t new_vcpu_count) const -{ - return OperationResult{E_NOTIMPL, L"Not implemented yet!"}; -} - -// --------------------------------------------------------- - OperationResult HCSWrapper::get_compute_system_properties(const std::string& compute_system_name) const { diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h index 14cd8bc5ab..f7899eb721 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api_wrapper.h @@ -160,20 +160,6 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- - /** - * Change the amount of available vCPUs in the compute system - * - * @param compute_system_name Target compute system name - * @param new_size_mib New memory size, in megabytes - * - * @return An object that evaluates to true on success, false otherwise. - * message() may contain details of failure when result is false. - */ - [[nodiscard]] OperationResult update_cpu_count(const std::string& compute_system_name, - std::uint32_t new_vcpu_count) const override; - - // --------------------------------------------------------- - /** * Retrieve the current state of the compute system. * diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h index 347f6d4db0..38ff408e6a 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -24,7 +24,6 @@ #include #include - #include #include diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index f943ce09a8..80603593bf 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h @@ -47,8 +47,6 @@ struct HCSWrapperInterface const std::filesystem::path& file_path) const = 0; virtual OperationResult revoke_vm_access(const std::string& compute_system_name, const std::filesystem::path& file_path) const = 0; - virtual OperationResult update_cpu_count(const std::string& compute_system_name, - const std::uint32_t new_core_count) const = 0; virtual OperationResult get_compute_system_state(const std::string& compute_system_name, ComputeSystemState& state_out) const = 0; virtual OperationResult modify_compute_system(const std::string& compute_system_name, diff --git a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 34b4ebf76a..fc3465bf35 100644 --- a/tests/hyperv_api/test_it_hyperv_hcs_api.cpp +++ b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -130,47 +130,6 @@ TEST_F(HyperVHCSAPI_IntegrationTests, enumerate_properties) ASSERT_FALSE(d_result.status_msg.empty()); } -TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) -{ - - uut_t uut{}; - - hyperv::hcs::CreateComputeSystemParameters params{}; - params.name = "test"; - params.memory_size_mb = 1024; - params.processor_count = 1; - params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}); - params.scsi_devices.push_back(hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}); - - const auto c_result = uut.create_compute_system(params); - - ASSERT_TRUE(c_result); - ASSERT_TRUE(c_result.status_msg.empty()); - - const auto s_result = uut.start_compute_system(params.name); - ASSERT_TRUE(s_result); - ASSERT_TRUE(s_result.status_msg.empty()); - - const auto p_result = uut.get_compute_system_properties(params.name); - EXPECT_TRUE(p_result); - std::wprintf(L"%s\n", p_result.status_msg.c_str()); - - const auto u_result = uut.update_cpu_count(params.name, 8); - EXPECT_TRUE(u_result); - auto v = fmt::to_string(u_result.code); - std::wprintf(L"%s\n", u_result.status_msg.c_str()); - std::printf("%s \n", v.c_str()); - - // const auto e_result = uut.enumerate_all_compute_systems(); - // EXPECT_TRUE(e_result); - // std::wprintf(L"%s\n", e_result.status_msg.c_str()); - - const auto d_result = uut.terminate_compute_system(params.name); - ASSERT_TRUE(d_result); - std::wprintf(L"%s\n", d_result.status_msg.c_str()); - ASSERT_FALSE(d_result.status_msg.empty()); -} - TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) { From c7e32043861d97b952f86119cbc95b2bbb35db6c Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 28 May 2025 16:33:35 +0300 Subject: [PATCH 72/76] [hyperv-hcs] introduce HcsPath type HcsPath is a std::filesystem::path wrapper for ensuring proper path formatting for the Host Compute System API. --- .../backends/hyperv_api/CMakeLists.txt | 1 + .../hyperv_api/hcs/hyperv_hcs_path.cpp | 42 +++++++++++++ .../backends/hyperv_api/hcs/hyperv_hcs_path.h | 63 +++++++++++++++++++ .../hcs/hyperv_hcs_plan9_share_params.cpp | 3 +- .../hcs/hyperv_hcs_plan9_share_params.h | 7 ++- .../hyperv_api/hcs/hyperv_hcs_scsi_device.h | 4 +- .../hyperv_api/hcs_virtual_machine.cpp | 2 +- 7 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.cpp create mode 100644 src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.h diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index f1eaab1686..13901b5b96 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -52,6 +52,7 @@ if(WIN32) hcs/hyperv_hcs_plan9_share_params.cpp hcs/hyperv_hcs_create_compute_system_params.cpp hcs/hyperv_hcs_request.cpp + hcs/hyperv_hcs_path.cpp virtdisk/virtdisk_api_wrapper.cpp virtdisk/virtdisk_snapshot.cpp hcs_plan9_mount_handler.cpp diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.cpp new file mode 100644 index 0000000000..3f3f6a9d14 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +using multipass::hyperv::hcs::HcsPath; + +template +template +auto fmt::formatter::format(const HcsPath& path, FormatContext& ctx) const -> + typename FormatContext::iterator +{ + if constexpr (std::is_same_v) + { + return format_to(ctx.out(), "{}", path.get().generic_string()); + } + else if constexpr (std::is_same_v) + { + return format_to(ctx.out(), L"{}", path.get().generic_wstring()); + } +} + +template auto fmt::formatter::format(const HcsPath&, fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const HcsPath&, + fmt::wformat_context&) const + -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.h new file mode 100644 index 0000000000..a555fdbfc5 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.h @@ -0,0 +1,63 @@ +/* + * 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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCS_PATH_H +#define MULTIPASS_HYPERV_API_HCS_PATH_H + +#include + +#include +#include + +namespace multipass::hyperv::hcs +{ +/** + * Host Compute System API expects paths + * to be formatted in a certain way. HcsPath + * is a strong type that ensures the correct + * formatting. + */ +struct HcsPath +{ + template + HcsPath(Args&&... args) : value{std::forward(args)...} + { + } + + template + HcsPath& operator=(T&& v) + { + value = std::forward(v); + return *this; + } + const std::filesystem::path& get() const noexcept { return value; } +private: + std::filesystem::path value; +}; +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for Path + */ +template +struct fmt::formatter : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsPath&, FormatContext&) const -> typename FormatContext::iterator; +}; + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp index c05cac9756..8cfb821b69 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp @@ -39,11 +39,10 @@ auto fmt::formatter::format(const HcsAddPlan9S "Flags": {4} }} )json"); - const auto path_str = params.host_path.generic_string(); return format_to(ctx.out(), json_template.as(), maybe_widen{params.name}, - maybe_widen{path_str}, + params.host_path, params.port, maybe_widen{params.access_name}, fmt::underlying(params.flags)); diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h index bbe32770e9..775f680e28 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -18,8 +18,11 @@ #ifndef MULTIPASS_HYPERV_API_HCS_PLAN9_SHARE_PARAMS_H #define MULTIPASS_HYPERV_API_HCS_PLAN9_SHARE_PARAMS_H -#include +#include + #include + +#include #include namespace multipass::hyperv::hcs @@ -71,7 +74,7 @@ struct HcsAddPlan9ShareParameters : public detail::HcsPlan9Base /** * Host directory to share */ - std::filesystem::path host_path{}; + HcsPath host_path{}; /** * ReadOnly 0x00000001 diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h index ff0d6dd074..113e7e3d30 100644 --- a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.h @@ -18,11 +18,11 @@ #ifndef MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H #define MULTIPASS_HYPERV_API_HCS_SCSI_DEVICE_H +#include #include #include -#include #include namespace multipass::hyperv::hcs @@ -32,7 +32,7 @@ struct HcsScsiDevice { HcsScsiDeviceType type; std::string name; - std::filesystem::path path; + HcsPath path; bool read_only; }; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index ddf7b01f20..3c57ea0906 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -337,7 +337,7 @@ bool HCSVirtualMachine::maybe_create_compute_system() // Grant access to the VHDX and the cloud-init ISO files. for (const auto& scsi : create_compute_system_params.scsi_devices) { - grant_access_to_paths({scsi.path}); + grant_access_to_paths({scsi.path.get()}); } return true; From 02665f4b582a48a4fb36236ff0fd2e54bc46dd15 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 28 May 2025 17:46:22 +0300 Subject: [PATCH 73/76] [platform/win] rewrite get_username using native Win32 API --- CMakeLists.txt | 1 + src/platform/CMakeLists.txt | 3 ++- src/platform/platform_win.cpp | 17 +++++++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3eaf8896c0..1cab7e30a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,6 +277,7 @@ if(MSVC) add_definitions(-DLIBSSH_STATIC) # otherwise adds declspec specifiers to libssh apis add_definitions(-D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS) add_definitions(-DWIN32_LEAN_AND_MEAN) + add_definitions(-DSECURITY_WIN32) set(MULTIPASS_BACKENDS hyperv hyperv_api virtualbox) set(MULTIPASS_PLATFORM windows) else() diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt index 445f933fa7..145475ec0f 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -29,7 +29,8 @@ function(add_target TARGET_NAME) jsoncpp_static shared_win scope_guard - wineventlogger) + wineventlogger + Secur32) elseif(APPLE) add_library(${TARGET_NAME} STATIC platform_osx.cpp diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 07a43a33f4..8dde5ab7a5 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -60,6 +60,8 @@ #include #include #include +#include +#include #include #include @@ -1073,12 +1075,15 @@ int mp::platform::Platform::utime(const char* path, int atime, int mtime) const QString mp::platform::Platform::get_username() const { - QString username; - mp::PowerShell::exec({"((Get-WMIObject -class Win32_ComputerSystem | Select-Object -ExpandProperty username))"}, - "get-username", - &username); - return username.section('\\', 1); -} + DWORD needed_size = 0; + GetUserNameEx(EXTENDED_NAME_FORMAT::NameSamCompatible, nullptr, &needed_size); + std::unique_ptr buff(new wchar_t[needed_size]); + if (GetUserNameExW(EXTENDED_NAME_FORMAT::NameSamCompatible, buff.get(), &needed_size)) + { + return QString::fromWCharArray(buff.get(), needed_size); + } + throw std::runtime_error("Failed retrieving user name!"); +}+ QDir mp::platform::Platform::get_alias_scripts_folder() const { From f289bf381a95c3a1866af1b61e682b09b2e41246 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 30 May 2025 11:55:17 +0300 Subject: [PATCH 74/76] [smb_mount] use Win32 API for folder permission checks --- .../backends/shared/windows/CMakeLists.txt | 3 +- .../shared/windows/smb_mount_handler.cpp | 87 ++++++++++++++++--- src/platform/platform_win.cpp | 11 ++- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/platform/backends/shared/windows/CMakeLists.txt b/src/platform/backends/shared/windows/CMakeLists.txt index ad480e8c9b..fd4fd7e010 100644 --- a/src/platform/backends/shared/windows/CMakeLists.txt +++ b/src/platform/backends/shared/windows/CMakeLists.txt @@ -28,4 +28,5 @@ target_link_libraries(shared_win logger OpenSSL::Crypto sftp_client - utils) + utils + ztd::out_ptr) diff --git a/src/platform/backends/shared/windows/smb_mount_handler.cpp b/src/platform/backends/shared/windows/smb_mount_handler.cpp index 524c369649..6288e67d34 100644 --- a/src/platform/backends/shared/windows/smb_mount_handler.cpp +++ b/src/platform/backends/shared/windows/smb_mount_handler.cpp @@ -29,12 +29,20 @@ #include #include +#include +#include +#include #include +#include + #pragma comment(lib, "Netapi32.lib") namespace mp = multipass; namespace mpl = multipass::logging; +using ztd::out_ptr::out_ptr; + +using sid_buffer = std::vector; namespace { @@ -60,6 +68,70 @@ catch (const mp::ExitlessSSHProcessException&) mpl::log(mpl::Level::info, category, fmt::format("Timeout while installing 'cifs-utils' in '{}'", name)); throw std::runtime_error("Timeout installing cifs-utils"); } + +/** + * Retrieve SID of given user name. + * + * @param [in] user_name The user name + * @return std::wstring User's SID as wide string + */ +sid_buffer get_user_sid(const std::wstring& user_name) +{ + DWORD sid_size = 0, domain_size = 0; + SID_NAME_USE sid_use{}; + LookupAccountNameW(nullptr, user_name.c_str(), nullptr, &sid_size, nullptr, &domain_size, &sid_use); + + std::vector sid(sid_size); + std::wstring domain(domain_size, wchar_t('\0')); + if (!LookupAccountNameW(nullptr, user_name.c_str(), sid.data(), &sid_size, domain.data(), &domain_size, &sid_use)) + throw std::runtime_error("LookupAccountName failed"); + return sid; +} + +/** + * Check whether given user has full control over the path. + * + * @param [in] path The target path + * @param [in] user_sid User's SID + * + * @return true if user @p user_sid has full control, false otherwise. + */ +bool has_full_control(const std::filesystem::path& path, sid_buffer& user_sid) +{ + std::unique_ptr pSD{nullptr, LocalFree}; + PACL pDACL = nullptr; + + DWORD result = GetNamedSecurityInfoW(path.c_str(), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + &pDACL, + nullptr, + out_ptr(pSD)); + + if (result != ERROR_SUCCESS) + throw std::runtime_error("Failed to get security info"); + + for (DWORD i = 0; i < pDACL->AceCount; ++i) + { + LPVOID pAce = nullptr; + if (!GetAce(pDACL, i, &pAce)) + continue; + + auto ace = reinterpret_cast(pAce); + if (ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE) + continue; + + if (!EqualSid(reinterpret_cast(&ace->SidStart), reinterpret_cast(user_sid.data()))) + continue; + + if ((ace->Mask & FILE_ALL_ACCESS) == FILE_ALL_ACCESS) + return true; + } + return false; +} + } // namespace namespace multipass @@ -78,17 +150,8 @@ void SmbManager::create_share(const QString& share_name, const QString& source, if (share_exists(share_name)) return; - // TODO: I tried to use the proper Windows API to get ACL permissions for the user being passed in, but - // alas, the API is very convoluted. At some point, another attempt should be made to use the proper API though... - QString user_access_output; - const auto user_access_res = PowerShell::exec( - {QString{"(Get-Acl '%1').Access | ?{($_.IdentityReference -match '%2') -and ($_.FileSystemRights " - "-eq 'FullControl')}"} - .arg(source, user)}, - "Get ACLs", - &user_access_output); - - if (!user_access_res || user_access_output.isEmpty()) + auto user_sid = get_user_sid(user.toStdWString()); + if (!has_full_control(source.toStdString(), user_sid)) throw std::runtime_error{fmt::format("cannot access \"{}\"", source)}; std::wstring remark = L"Multipass mount share"; @@ -96,7 +159,7 @@ void SmbManager::create_share(const QString& share_name, const QString& source, auto wide_source = source.toStdWString(); DWORD parm_err = 0; - SHARE_INFO_2 share_info; + SHARE_INFO_2 share_info = {}; share_info.shi2_netname = wide_share_name.data(); share_info.shi2_remark = remark.data(); share_info.shi2_type = STYPE_DISKTREE; diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 8dde5ab7a5..1a6c783719 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -1075,15 +1075,14 @@ int mp::platform::Platform::utime(const char* path, int atime, int mtime) const QString mp::platform::Platform::get_username() const { - DWORD needed_size = 0; - GetUserNameEx(EXTENDED_NAME_FORMAT::NameSamCompatible, nullptr, &needed_size); - std::unique_ptr buff(new wchar_t[needed_size]); - if (GetUserNameExW(EXTENDED_NAME_FORMAT::NameSamCompatible, buff.get(), &needed_size)) + wchar_t username_buf[UNLEN + 1] = {}; + DWORD sz = sizeof(username_buf) / sizeof(wchar_t); + if (GetUserNameW(username_buf, &sz)) { - return QString::fromWCharArray(buff.get(), needed_size); + return QString::fromWCharArray(username_buf, sz); } throw std::runtime_error("Failed retrieving user name!"); -}+ +} QDir mp::platform::Platform::get_alias_scripts_folder() const { From c0df204579677ac2693f55ea77dab0eac9caa382 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 30 May 2025 11:57:23 +0300 Subject: [PATCH 75/76] [smb_mount] sanitize share name --- src/platform/backends/shared/windows/smb_mount_handler.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform/backends/shared/windows/smb_mount_handler.cpp b/src/platform/backends/shared/windows/smb_mount_handler.cpp index 6288e67d34..814f889b8c 100644 --- a/src/platform/backends/shared/windows/smb_mount_handler.cpp +++ b/src/platform/backends/shared/windows/smb_mount_handler.cpp @@ -239,9 +239,8 @@ SmbMountHandler::SmbMountHandler(VirtualMachine* vm, source{QString::fromStdString(get_mount_spec().get_source_path())}, // share name must be unique and 80 chars max share_name{ - QString("%1_%2:%3") - .arg(MP_UTILS.make_uuid(target), QString::fromStdString(vm->vm_name), QString::fromStdString(target)) - .left(80)}, + // 73 chars in total. Less than < 80 chars for max SMB share name length. + QString::fromStdString(fmt::format("{}-{}", MP_UTILS.make_uuid(vm->vm_name), MP_UTILS.make_uuid(target)))}, cred_dir{cred_dir}, smb_manager{&smb_manager} { From 2de0b22c639079360c6c22beeb1d9baa5a3dd416 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 30 May 2025 11:57:55 +0300 Subject: [PATCH 76/76] [hyperv-hcs] use smb mount handler as native mount handler --- .../backends/hyperv_api/hcs_virtual_machine.cpp | 16 ++++++++++++---- 1 file changed, 12 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 3c57ea0906..3fe807ac7d 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include @@ -561,10 +562,17 @@ std::unique_ptr HCSVirtualMachine::make_native_mount_handler(const const VMMount& mount) { mpl::debug(kLogCategory, "make_native_mount_handler() -> called for VM `{}`, target: {}", vm_name, target); - - throw NotImplementedOnThisBackendException{ - "Plan9 mounts require an agent running on guest, which is not implemented yet."}; - // return std::make_unique(this, &key_provider, mount, target, hcs); + // throw NotImplementedOnThisBackendException{ + // "Plan9 mounts require an agent running on guest, which is not implemented yet."}; + // FIXME: Replace with Plan9 mount handler once the guest agent is available. + + static const SmbManager smb_manager{}; + return std::make_unique(this, + &key_provider, + target, + mount, + instance_dir.absolutePath(), + smb_manager); } std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const std::string& snapshot_name,