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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3901c38c46..1cab7e30a4 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) @@ -275,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/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/platform/backends/hyperv_api/hyperv_api_common.cpp b/include/multipass/platform_win.h similarity index 50% rename from src/platform/backends/hyperv_api/hyperv_api_common.cpp rename to include/multipass/platform_win.h index 133745ea59..53384011f9 100644 --- a/src/platform/backends/hyperv_api/hyperv_api_common.cpp +++ b/include/multipass/platform_win.h @@ -15,15 +15,41 @@ * */ -#include -#include +#ifndef MULTIPASS_PLATFORM_WIN_H +#define MULTIPASS_PLATFORM_WIN_H -#include +#include -#include // for CLSIDFromString +#include -#include -#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}; +}; + +} // namespace multipass::platform /** * Formatter for GUID type @@ -63,77 +89,4 @@ struct fmt::formatter<::GUID, Char> } }; -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 +#endif // MULTIPASS_PLATFORM_WIN_H diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 57e5b302b1..5dbb36261c 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -28,12 +28,15 @@ #include #include +#include #include #include #include #include #include +#include + namespace multipass { class MemorySize; @@ -78,7 +81,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; @@ -102,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; @@ -145,4 +149,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 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/cmake/cmake-deps.cmake b/src/cmake/cmake-deps.cmake new file mode 100644 index 0000000000..944bf22244 --- /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) 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/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt index ccfdf570df..13901b5b96 100644 --- a/src/platform/backends/hyperv_api/CMakeLists.txt +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -41,14 +41,28 @@ 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 + hcn/hyperv_hcn_ipam.cpp + hcn/hyperv_hcn_network_policy.cpp 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 + 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 + hcs_virtual_machine.cpp + hcs_virtual_machine_factory.cpp ) 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 7a5394bff2..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,8 +20,8 @@ #include #include #include -#include +#include #include #include @@ -35,14 +35,22 @@ // clang-format on #include +#include #include #include #include +using ztd::out_ptr::out_ptr; + namespace multipass::hyperv::hcn { +struct GuidParseError : multipass::FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + namespace { @@ -62,6 +70,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 * @@ -85,16 +152,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)..., out_ptr(result_msgbuf, api.CoTaskMemFree))}; mpl::debug(kLogCategory, "perform_operation(...) > fn: {}, result: {}", @@ -123,14 +187,36 @@ 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), out_ptr(network, api.CloseNetwork)); if (!result) { mpl::error(kLogCategory, "open_network() > HcnOpenNetwork failed with {}!", result.code); } - return UniqueHcnNetwork{network, api.CloseNetwork}; + return network; +} + +/** + * 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; } } // namespace @@ -153,44 +239,47 @@ 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)); + 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","))); - 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); + out_ptr(network, api.CloseNetwork)); 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, + static_cast(result.code)); } - [[maybe_unused]] UniqueHcnNetwork _{network, api.CloseNetwork}; return result; } @@ -224,28 +313,24 @@ OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& para "Major": 2, "Minor": 16 }}, - "HostComputeNetwork": "{0}", - "Policies": [ - ], - "IpConfigurations": [ - {{ - "IpAddress": "{1}" - }} - ] + "HostComputeNetwork": "{HostComputeNetwork}", + "Policies": [], + "MacAddress" : {MacAddress} }})"; // 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)); - HCN_ENDPOINT endpoint{nullptr}; + const auto endpoint_settings = fmt::format( + endpoint_settings_template, + 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")); + 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}; + out_ptr(endpoint, api.CloseEndpoint)); return result; } 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..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 @@ -44,9 +46,12 @@ struct CreateEndpointParameters std::string endpoint_guid{}; /** - * The IPv[4-6] address to assign to the endpoint. + * MAC address assocaited with the endpoint (optional). + * + * HCN will auto-assign a MAC address to the endpoint when + * not specified, where applicable. */ - std::string endpoint_ipvx_addr{}; + std::optional mac_address; }; } // namespace multipass::hyperv::hcn @@ -66,10 +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 IPvX Addr.: ({})", + "Endpoint GUID: ({}) | Network GUID: ({}) | MAC address: ({})", params.endpoint_guid, params.network_guid, - params.endpoint_ipvx_addr); + params.mac_address); } }; 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.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp new file mode 100644 index 0000000000..f54be22473 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp @@ -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 . + * + */ + +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnIpam; + +template +template +auto fmt::formatter::format(const 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(), + maybe_widen{ipam.type}, + fmt::join(ipam.subnets, comma.as())); +} + +template auto fmt::formatter::format(const HcnIpam&, fmt::format_context&) const + -> fmt::format_context::iterator; + +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_ipam.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h new file mode 100644 index 0000000000..57dc8c462d --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.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_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 : formatter, Char> +{ + template + 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_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..2303491829 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h @@ -0,0 +1,118 @@ +/* + * 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, ///< ?? +}; + +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 + +/** + * 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.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp new file mode 100644 index 0000000000..e9815bf75c --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp @@ -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 . + * + */ + +#include +#include +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcn::HcnNetworkPolicy; +using multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName; + +template +struct NetworkPolicySettingsFormatters +{ + 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(), maybe_widen{policy.net_adapter_name}); + } +}; + +template +template +auto fmt::formatter::format(const 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(), + maybe_widen{policy.type}, + std::visit(NetworkPolicySettingsFormatters{}, policy.settings)); +} + +template auto fmt::formatter::format(const HcnNetworkPolicy&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +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.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h new file mode 100644 index 0000000000..5a605f0eb8 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.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_NETWORK_POLICY_H +#define MULTIPASS_HYPERV_API_HCN_NETWORK_POLICY_H + +#include +#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 : formatter, Char> +{ + template + 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_network_policy_netadaptername.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h new file mode 100644 index 0000000000..75afa605de --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.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_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 + +#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..e65df896d6 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_type.h @@ -0,0 +1,168 @@ +/* + * 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.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp new file mode 100644 index 0000000000..94e6b937b6 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.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::hcn::HcnRoute; + +template +template +auto fmt::formatter::format(const 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(), + maybe_widen{route.next_hop}, + maybe_widen(route.destination_prefix), + route.metric); +} + +template auto fmt::formatter::format(const HcnRoute&, fmt::format_context&) const + -> fmt::format_context::iterator; + +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_route.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h new file mode 100644 index 0000000000..9eff7b4860 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.h @@ -0,0 +1,57 @@ +/* + * 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 + +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 : formatter, Char> +{ + template + 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..97be8a34e0 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp @@ -0,0 +1,54 @@ +/* + * 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::hcn::HcnSubnet; + +template +template +auto fmt::formatter::format(const 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()), + maybe_widen{subnet.ip_address_prefix}); +} + +template auto fmt::formatter::format(const HcnSubnet&, fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format(const 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 new file mode 100644 index 0000000000..b718985781 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.h @@ -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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HCN_SUBNET_H +#define MULTIPASS_HYPERV_API_HCN_SUBNET_H + +#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 : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcn::HcnSubnet& route, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +#endif 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 34c6482e87..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 @@ -14,13 +14,12 @@ * along with this program. If not, see . * */ +#include -#include #include -#include #include -#include #include +#include #include @@ -33,6 +32,9 @@ #include #include +#include + +using ztd::out_ptr::out_ptr; namespace multipass::hyperv::hcs { @@ -68,7 +70,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}; } @@ -91,19 +93,21 @@ OperationResult wait_for_operation_result(const HCSAPITable& api, fmt::ptr(op.get()), timeout.count()); - wchar_t* result_msg_out{nullptr}; - const auto result = 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""}; + 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()), + 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; } // --------------------------------------------------------- @@ -121,24 +125,52 @@ 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}; - const auto result = ResultCode{api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, &system)}; - + UniqueHcsSystem system{}; + const ResultCode result = + api.OpenComputeSystem(name_w.c_str(), kRequestedAccessLevel, out_ptr(system, api.CloseComputeSystem)); if (!result) { - mpl::error(kLogCategory, + mpl::debug(kLogCategory, "open_host_compute_system(...) > failed to open ({}), result code: ({})", name, result); } - return UniqueHcsSystem{system, api.CloseComputeSystem}; + return system; } // --------------------------------------------------------- +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. * @@ -168,41 +200,17 @@ 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) { - mpl::error(kLogCategory, + mpl::debug(kLogCategory, "perform_hcs_operation(...) > HcsOpenComputeSystem failed! {}", target_hcs_system_name); - return OperationResult{E_POINTER, 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!"}; + return OperationResult{E_INVALIDARG, L"HcsOpenComputeSystem 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 @@ -220,93 +228,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::wstring result = {}; - 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); - } - - if (!params.vhdx_path.empty()) - { - result += fmt::format(scsi_device_template, - L"Primary disk", - L"VirtualDisk", - string_to_wstring(params.vhdx_path), - false); - } - 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 - 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} - }} - }} - }} - }})"; - - // 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); - HCS_SYSTEM system{nullptr}; - auto operation = create_operation(api); if (nullptr == operation) @@ -314,12 +235,16 @@ OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParam return OperationResult{E_POINTER, L"HcsCreateOperation failed."}; } - const auto name_w = string_to_wstring(params.name); - const auto result = - ResultCode{api.CreateComputeSystem(name_w.c_str(), vm_settings.c_str(), operation.get(), nullptr, &system)}; + const std::wstring name_w = maybe_widen{params.name}; + // Render the template + const auto vm_settings = fmt::to_wstring(params); - // Auto-release the system handle - [[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, + out_ptr(system, api.CloseComputeSystem))}; if (!result) { @@ -342,7 +267,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); } // --------------------------------------------------------- @@ -378,80 +310,6 @@ OperationResult HCSWrapper::resume_compute_system(const std::string& compute_sys // --------------------------------------------------------- -OperationResult HCSWrapper::add_endpoint(const AddEndpointParameters& params) const -{ - mpl::debug(kLogCategory, "add_endpoint(...) > params: {}", params); - constexpr auto add_endpoint_settings_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", - "RequestType": "Add", - "Settings": {{ - "EndpointId": "{0}", - "MacAddress": "{1}", - "InstanceId": "{0}" - }} - }})"; - - const auto settings = fmt::format(add_endpoint_settings_template, - string_to_wstring(params.endpoint_guid), - string_to_wstring(params.nic_mac_address)); - - return perform_hcs_operation(api, - api.ModifyComputeSystem, - params.target_compute_system_name, - settings.c_str(), - nullptr); -} - -// --------------------------------------------------------- - -OperationResult HCSWrapper::remove_endpoint(const std::string& compute_system_name, - const std::string& endpoint_guid) const -{ - mpl::debug(kLogCategory, - "remove_endpoint(...) > name: ({}), endpoint_guid: ({})", - compute_system_name, - endpoint_guid); - - constexpr auto remove_endpoint_settings_template = LR"( - {{ - "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{{{0}}}", - "RequestType": "Remove" - }})"; - - const auto settings = fmt::format(remove_endpoint_settings_template, string_to_wstring(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. - 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!"}; -} - -// --------------------------------------------------------- - OperationResult HCSWrapper::get_compute_system_properties(const std::string& compute_system_name) const { @@ -476,8 +334,8 @@ 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 csname_as_wstring = string_to_wstring(compute_system_name); + const auto path_as_wstring = file_path.generic_wstring(); + 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""}; } @@ -493,34 +351,53 @@ 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""}; } // --------------------------------------------------------- -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(); + const auto ccs = compute_system_state_from_string(state_str.toStdString()); + if (ccs) + { + return ccs.value(); + } + return ComputeSystemState::unknown; + } + return ComputeSystemState::stopped; + }(); + + return {result.code, L""}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::modify_compute_system(const std::string& compute_system_name, + const HcsRequest& params) const +{ + mpl::debug(kLogCategory, "modify_compute_system(...) > params: {}", params); - return {result.code, L"Unknown"}; + 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 d08f7a0753..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 @@ -161,72 +161,30 @@ struct HCSWrapper : public HCSWrapperInterface // --------------------------------------------------------- /** - * Add a network endpoint 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] params Endpoint 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; - - // --------------------------------------------------------- - - /** - * Remove a network endpoint 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_endpoint(const std::string& compute_system_name, - const std::string& endpoint_guid) const override; - - // --------------------------------------------------------- - - /** - * Resize the amount of memory the compute system has. + * Retrieve the current state of the compute system. * - * @param compute_system_name Target compute system name - * @param new_size_mib New memory size, in megabytes + * @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 resize_memory(const std::string& compute_system_name, - std::uint32_t new_size_mib) const override; + [[nodiscard]] OperationResult get_compute_system_state(const std::string& compute_system_name, + ComputeSystemState& state_out) const override; // --------------------------------------------------------- /** - * Change the amount of available vCPUs in the compute system + * Modify a Host Compute System request. * * @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. - * - * @param [in] compute_system_name Target compute system's 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 get_compute_system_state(const std::string& compute_system_name) const override; + [[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_compute_system_state.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h index a1dc83bb37..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 @@ -24,6 +24,8 @@ #include #include +#include + namespace multipass::hyperv::hcs { @@ -51,7 +53,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}, @@ -69,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/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 365069f56d..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 @@ -18,7 +18,12 @@ #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 namespace multipass::hyperv::hcs @@ -45,14 +50,21 @@ struct CreateComputeSystemParameters std::uint32_t processor_count{}; /** - * Path to the cloud-init ISO file + * List of SCSI devices that are attached on boot */ - std::string cloudinit_iso_path{}; + std::vector scsi_devices; /** - * Path to the Primary (boot) VHDX file + * List of endpoints that'll be added to the compute system + * by default at creation time. */ - std::string vhdx_path{}; + std::vector network_adapters{}; + + /** + * List of Plan9 shares that'll be added to the compute system + * by default at creation time. + */ + std::vector shares{}; }; } // namespace multipass::hyperv::hcs @@ -62,24 +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) | cloud-init ISO path: " - "({}) | VHDX path: ({})", - params.name, - params.processor_count, - params.memory_size_mb, - params.cloudinit_iso_path, - params.vhdx_path); - } + 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/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_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/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 new file mode 100644 index 0000000000..8cfb821b69 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp @@ -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 . + * + */ +#include + +#include + +#include + +using multipass::hyperv::maybe_widen; +using multipass::hyperv::hcs::HcsAddPlan9ShareParameters; +using multipass::hyperv::hcs::HcsRemovePlan9ShareParameters; + +template +template +auto fmt::formatter::format(const HcsAddPlan9ShareParameters& 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 +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 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 new file mode 100644 index 0000000000..775f680e28 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -0,0 +1,112 @@ +/* + * 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 +#include + +namespace multipass::hyperv::hcs +{ + +enum class Plan9ShareFlags : std::uint32_t +{ + none = 0, + read_only = 0x00000001, + linux_metadata = 0x00000004, + case_sensitive = 0x00000008 +}; + +namespace detail +{ +struct HcsPlan9Base +{ + /** + * 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{}; + + /** + * Target port. + */ + std::uint16_t port{default_port}; +}; +} // namespace detail + +struct HcsRemovePlan9ShareParameters : public detail::HcsPlan9Base +{ +}; + +struct HcsAddPlan9ShareParameters : public detail::HcsPlan9Base +{ + /** + * Host directory to share + */ + HcsPath host_path{}; + + /** + * ReadOnly 0x00000001 + * LinuxMetadata 0x00000004 + * CaseSensitive 0x00000008 + */ + Plan9ShareFlags flags{Plan9ShareFlags::none}; +}; +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for Plan9ShareParameters + */ +template +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::HcsRemovePlan9ShareParameters& param, FormatContext& ctx) const -> + typename FormatContext::iterator; +}; + +#endif // MULTIPASS_HYPERV_API_HCS_ADD_9P_SHARE_PARAMS_H 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..8cdd8a4aec --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp @@ -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 . + * + */ +#include + +#include + +#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"); + 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}); + } + + auto operator()(const HcsModifyMemorySettings& params) + { + return to_string(params.size_in_mb); + } + + auto operator()(const HcsAddPlan9ShareParameters& params) + { + return to_string(params); + } + + auto operator()(const HcsRemovePlan9ShareParameters& params) + { + return to_string(params); + } +}; + +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..38ff408e6a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -0,0 +1,62 @@ +/* + * 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 + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +/** + * @brief HcsRequest type for HCS modifications + */ +struct HcsRequest +{ + HcsResourcePath resource_path; + HcsRequestType request_type; + std::variant + settings{std::monostate{}}; +}; + +} // 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..ec4660e89c --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.h @@ -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 . + * + */ + +#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"}; + } + + constexpr static auto Update() + { + return HcsRequestType{"Update"}; + } + +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..6f4d3a3ab4 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.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_HCS_RESOURCE_PATH_H +#define MULTIPASS_HYPERV_API_HCS_RESOURCE_PATH_H + +#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) + { + return fmt::format("VirtualMachine/Devices/NetworkAdapters/{{{0}}}", network_adapter_id); + } + + static HcsResourcePath Memory() + { + return std::string{"VirtualMachine/ComputeTopology/Memory/SizeInMB"}; + } + + static HcsResourcePath Plan9Shares() + { + return std::string{"VirtualMachine/Devices/Plan9/Shares"}; + } + +private: + HcsResourcePath(std::string v) : value(v) + { + } + + std::string value{}; +}; + +} // namespace multipass::hyperv::hcs + +#endif diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.cpp new file mode 100644 index 0000000000..c45878ab93 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.cpp @@ -0,0 +1,58 @@ +/* + * 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::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..113e7e3d30 --- /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; + HcsPath 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/hyperv_hcs_wrapper_interface.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper_interface.h index ea1f7348ef..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 @@ -18,8 +18,11 @@ #ifndef MULTIPASS_HYPERV_API_HCS_WRAPPER_INTERFACE_H #define MULTIPASS_HYPERV_API_HCS_WRAPPER_INTERFACE_H -#include +#include #include +#include +#include +#include #include #include @@ -44,14 +47,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 AddEndpointParameters& 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, - 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 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/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..cc5725ea1f --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp @@ -0,0 +1,166 @@ +/* + * 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) +{ + // 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 hcs::HcsRequest{hcs::HcsResourcePath::Plan9Shares(), hcs::HcsRequestType::Add(), params}; + }(); + + const auto result = hcs->modify_compute_system(vm->vm_name, req); + + 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& 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 `{}`", req, 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 (...) + { + 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 + } + } +} +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 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)) + { + 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 new file mode 100644 index 0000000000..3fe807ac7d --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -0,0 +1,599 @@ +/* + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +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; +namespace mpp = multipass::platform; +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 = "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(), ':', '-'); +} + +/** + * Perform a DNS resolve of @p hostname to obtain IPv4/IPv6 + * address(es) associated with it. + * + * @param [in] hostname Hostname to resolve + * @return Vector of IPv4/IPv6 addresses + */ +auto resolve_ip_addresses(const std::string& hostname) +{ + const static mpp::wsa_init_wrapper wsa_context{}; + + std::vector ipv4{}, ipv6{}; + mpl::trace(kLogCategory, "resolve_ip_addresses() -> resolve being called for hostname `{}`", hostname); + + 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] = [&]() { + 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}); + }(); + + if (result == 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: + { + 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::error( + kLogCategory, + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv4 address data while expecting {}!", + ptr->ai_addrlen, + kSockaddrInSize); + } + break; + case AF_INET6: + { + 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::error( + kLogCategory, + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv6 address data while expecting {}!", + ptr->ai_addrlen, + kSockaddrIn6Size); + } + break; + default: + continue; + } + } + } + + mpl::trace(kLogCategory, + "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", + hostname, + fmt::join(ipv4, ","), + fmt::join(ipv6, ",")); + + return std::make_pair(ipv4, ipv6); +} +} // namespace + +namespace multipass::hyperv +{ + +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), + virtdisk(std::move(virtdisk_w)) +{ + // Verify that the given API wrappers are not null + { + 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, ",")}; + } + } + + 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(state); + update_state(); +} + +std::filesystem::path HCSVirtualMachine::get_primary_disk_path() const +{ + 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; +} + +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); + } + } +} + +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))) + { + // Target compute system already exist, no need to re-create. + return false; + } + } + + // FIXME: Handle suspend state? + + const auto create_endpoint_params = [this]() { + std::vector params{}; + + // 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()); + 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()); + params.push_back(extra_endpoint); + } + return 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. + if (hcn->delete_endpoint(endpoint.endpoint_guid)) + { + mpl::debug(kLogCategory, + "The endpoint {} was already present for the VM {}, removed it.", + endpoint.endpoint_guid, + vm_name); + } + if (const auto& [status, msg] = hcn->create_endpoint(endpoint); !status) + { + throw CreateEndpointException{"create_endpoint failed with {}", status}; + } + } + + // E_INVALIDARG means there's no such VM. + // Create the VM from scratch. + 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; + + 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_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!"); + } + 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.network_adapters), + create_endpoint_to_network_adapter); + return params; + }(); + + 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. + for (const auto& scsi : create_compute_system_params.scsi_devices) + { + grant_access_to_paths({scsi.path.get()}); + } + + return true; +} + +void HCSVirtualMachine::set_state(hcs::ComputeSystemState 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) + { + 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) + return; + + mpl::info(kLogCategory, "set_state() > VM {} state changed from {} to {}", vm_name, prev_state, state); +} + +void HCSVirtualMachine::start() +{ + mpl::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 + // Try to determine whether we need to resume or start here. + const auto& [status, status_msg] = [&] { + // Fetch the latest state value. + const auto hcs_state = fetch_state_from_api(); + mpl::debug(kLogCategory, "start() -> VM `{}` HCS state is `{}`", vm_name, hcs_state); + switch (hcs_state) + { + case hcs::ComputeSystemState::paused: + { + 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::debug(kLogCategory, "start() -> VM `{}` is in {} state, starting", vm_name, state); + return hcs->start_compute_system(vm_name); + } + } + }(); + + 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) +{ + mpl::debug(kLogCategory, "shutdown() -> Shutting down VM `{}`, current state {}", vm_name, state); + + switch (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)) + { + // Fall back to SSH shutdown. + ssh_exec("sudo shutdown -h now"); + } + break; + case ShutdownPolicy::Halt: + case ShutdownPolicy::Poweroff: + 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); + break; + } + + state = State::off; + update_state(); +} + +void HCSVirtualMachine::suspend() +{ + 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(); +} + +HCSVirtualMachine::State HCSVirtualMachine::current_state() +{ + return state; +} +int HCSVirtualMachine::ssh_port() +{ + return kDefaultSSHPort; +} +std::string HCSVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) +{ + return fmt::format("{}.mshome.net", vm_name); +} +std::string HCSVirtualMachine::ssh_username() +{ + return description.ssh_username; +} + +std::string HCSVirtualMachine::management_ipv4() +{ + const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); + if (ipv4.empty()) + { + mpl::error(kLogCategory, "management_ipv4() > failed to resolve `{}`", ssh_hostname({})); + return "UNKNOWN"; + } + + const auto result = *ipv4.begin(); + + mpl::trace(kLogCategory, "management_ipv4() > IP address is `{}`", result); + // Prefer the first one + return result; +} +std::string HCSVirtualMachine::ipv6() +{ + const auto& [_, ipv6] = resolve_ip_addresses(ssh_hostname({}).c_str()); + if (ipv6.empty()) + { + // TODO: Log + return {}; + } + // Prefer the first one + return *ipv6.begin(); +} +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 HCSVirtualMachine::update_state() +{ + monitor.persist_state_for(vm_name, state); +} + +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 HCSVirtualMachine::update_cpus(int 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::debug(kLogCategory, + "resize_memory() -> called for VM `{}`, new_size `{}` MiB", + 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, + "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::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::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."}; + // 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, + const std::string& comment, + const std::string& instance_id, + const VMSpecs& specs, + std::shared_ptr parent) +{ + return std::make_shared(snapshot_name, + comment, + instance_id, + parent, + specs, + *this, + description, + virtdisk); +} + +std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const QString& filename) +{ + return std::make_shared(filename, *this, description, virtdisk); +} + +} // 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 new file mode 100644 index 0000000000..758ce0bb2a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -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 . + * + */ + +#ifndef MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H +#define MULTIPASS_HYPERV_API_HYPERV_VIRTUAL_MACHINE_H + +#include +#include + +#include +#include + +#include + +namespace multipass +{ +class VMStatusMonitor; +} + +namespace multipass::hyperv +{ + +/** + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. + */ +struct HCSVirtualMachine final : public BaseVirtualMachine +{ + 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(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; + 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; + +protected: + void require_snapshots_support() const override + { + } + + 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{}; + 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); + + /** + * 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); + + /** + * 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 + +#endif 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..c1d2dd1880 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h @@ -0,0 +1,68 @@ +/* + * 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; +}; + +struct StartComputeSystemException : 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 new file mode 100644 index 0000000000..cfcbf800c4 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -0,0 +1,301 @@ +/* + * 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 +#include + +#include +#include +#include +#include + +#include + +#include // for the std::filesystem::path formatter + +#include + +namespace multipass::hyperv +{ + +/** + * Category for the log messages. + */ +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) + : HCSVirtualMachineFactory(data_dir, + std::make_shared(), + std::make_shared(), + std::make_shared()) + +{ +} + +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) +{ + assert(hcs_sptr); + 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, + kDefaultHyperVSwitchGUID, + desc, + monitor, + key_provider, + get_instance_directory(desc.vm_name)); +} + +void HCSVirtualMachineFactory::remove_resources_for_impl(const std::string& name) +{ + 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) +{ + 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 HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, + const VirtualMachineDescription& desc) +{ + // Resize the instance image to the desired size + assert(virtdisk_sptr); + const auto& [status, status_msg] = + 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 `{}`", + instance_image.image_path.toStdString(), + status}; + } +} + +std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& 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(); + hcn::HcnNetworkPolicy policy{hcn::HcnNetworkPolicyType::NetAdapterName(), + hcn::HcnNetworkPolicyNetAdapterName{intf.id}}; + network_params.policies.push_back(policy); + 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; + } + + 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.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 + + 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); +} + +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 new file mode 100644 index 0000000000..6836115a27 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -0,0 +1,83 @@ +/* + * 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_FACTORY_H +#define MULTIPASS_HYPERV_API_HCS_VIRTUAL_MACHINE_FACTORY_H + +#include + +#include + +namespace multipass::hyperv +{ + +/** + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. + */ +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, + 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; + + 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; + +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}; + + /** + * Retrieve a list of available network adapters. + */ + static std::vector get_adapters(); +}; +} // namespace multipass::hyperv + +#endif 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/src/platform/backends/hyperv_api/hyperv_api_operation_result.h b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h index 0a906df477..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 @@ -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 @@ -123,7 +133,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/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/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..37a8b83e06 --- /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 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 b419919fd8..12eaf3f998 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,9 @@ // clang-format on #include + +#include +#include #include namespace multipass::hyperv::virtdisk @@ -31,18 +34,42 @@ 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) +{ + p.make_preferred(); + return p; +} using UniqueHandle = std::unique_ptr, decltype(VirtDiskAPITable::CloseHandle)>; 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()); // @@ -54,25 +81,27 @@ UniqueHandle open_virtual_disk(const VirtDiskAPITable& api, const std::filesyste 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 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)) + if (!result) { - 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}; } @@ -93,6 +122,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).generic_wstring(); // // https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Hyper-V/Storage/cpp/CreateVirtualDisk.cpp // @@ -102,40 +133,117 @@ 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{}; parameters.Version = CREATE_VIRTUAL_DISK_VERSION_2; parameters.Version2 = {}; parameters.Version2.MaximumSize = params.size_in_bytes; - - // - // 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 auto path_w = params.path.wstring(); + 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) { + std::filesystem::path pp{predecessor_path}; + if (!MP_FILEOPS.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") + { + 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 VirtDiskCreateError{"Unable to determine predecessor disk's (`{}`) type!", pp}; + } + else + { + throw VirtDiskCreateError{"Unsupported predecessor disk type"}; + } + } + else + { + throw VirtDiskCreateError{"Failed to retrieve the predecessor disk type for `{}`, error code: {}", + pp, + result}; + } + }; + + 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; + parameters.Version2.ParentVirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; + // Use parent's size. + parameters.Version2.MaximumSize = 0; + }, + }, + params.predecessor); 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, nullptr, // [in] CREATE_VIRTUAL_DISK_FLAG Flags, - CREATE_VIRTUAL_DISK_FLAG_NONE, + flags, // [in] ULONG ProviderSpecificFlags, 0, // [in] PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, @@ -198,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 { @@ -279,7 +460,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_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_create_virtual_disk_params.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h index 953157184c..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,12 +19,22 @@ #define MULTIPASS_HYPERV_API_VIRTDISK_CREATE_VIRTUAL_DISK_PARAMETERS_H #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. */ @@ -32,6 +42,7 @@ struct CreateVirtualDiskParameters { std::uint64_t size_in_bytes{}; std::filesystem::path path{}; + std::variant predecessor{}; }; } // namespace multipass::hyperv::virtdisk 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/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..f109a192e7 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.cpp @@ -0,0 +1,222 @@ +/* + * 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 +{ +constexpr auto kLogCategory = "virtdisk-snapshot"; +} + +namespace multipass::hyperv::virtdisk +{ + +struct CreateVirtdiskSnapshotError : public FormattedExceptionBase +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +VirtDiskSnapshot::VirtDiskSnapshot(const std::string& name, + const std::string& comment, + const std::string& instance_id, + std::shared_ptr parent, + const VMSpecs& specs, + 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} +{ +} + +std::string VirtDiskSnapshot::make_snapshot_filename(const Snapshot& ss) +{ + constexpr static auto kSnapshotNameFormat = "{}.avhdx"; + return fmt::format(kSnapshotNameFormat, ss.get_name()); +} + +std::filesystem::path VirtDiskSnapshot::make_snapshot_path(const Snapshot& ss) const +{ + return base_vhdx_path.parent_path() / make_snapshot_filename(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); + 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. + if (!MP_FILEOPS.exists(head_path)) + { + const auto& parent = get_parent(); + 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 + MP_FILEOPS.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 +{ + mpl::debug(kLogCategory, "create_new_child_disk() -> parent: {}, child: {}", parent, child); + assert(virtdisk); + // The parent must already exist. + 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 (MP_FILEOPS.exists(child)) + throw CreateVirtdiskSnapshotError{std::make_error_code(std::errc::file_exists), + "Child disk `{}` already exists", + child}; + + virtdisk::CreateVirtualDiskParameters params{}; + 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; + } + + 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 +{ + 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), + "Parent disk `{}` does not exist", + new_parent}; + assert(virtdisk); + for (const auto& child : snapshots) + { + const auto& child_path = make_snapshot_path(*child); + + if (MP_FILEOPS.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 `{}`: {}", + child_path, + new_parent, + static_cast(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); + 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) + { + const auto& parent_path = parent ? make_snapshot_path(*parent) : base_vhdx_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 + // 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{merge_r, "Could not merge differencing disk to parent"}; + } + // Finally, erase the merged disk. + mpl::debug(kLogCategory, "Removing snapshot file: `{}`", self_path); + MP_FILEOPS.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{}; + MP_FILEOPS.remove(head_path, ec); + mpl::debug(kLogCategory, "apply_impl() -> {} remove: {}", head_path, ec); + + // 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 new file mode 100644 index 0000000000..e492e2070b --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_snapshot.h @@ -0,0 +1,141 @@ +/* + * 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 + +#include + +#include + +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, + std::shared_ptr parent, + const VMSpecs& specs, + 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); + + /** + * 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"; + } + +protected: + void capture_impl() override; + void erase_impl() override; + 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}; +}; + +} // namespace multipass::hyperv::virtdisk + +#endif 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/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/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..814f889b8c 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; @@ -176,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} { diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index de6d732e3f..1a6c783719 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -16,10 +16,12 @@ */ #include +#include #include #include #include #include +#include #include #include #include @@ -27,7 +29,9 @@ #include #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" @@ -47,12 +51,22 @@ #include +#include +#include #include +#include +#include +#include +#include +#include #include +#include +#include #include #include #include +#include #include #include #include @@ -474,44 +488,285 @@ std::filesystem::path multipass_final_storage_location() } } // namespace +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; +}; + +struct InvalidNetworkPrefixLengthException : public multipass::FormattedExceptionBase<> +{ + 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] = {}; + if (!inet_ntop(AF_INET, &addr, str, sizeof(str))) + throw std::runtime_error("inet_ntop failed: errno"); + 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] = {}; + if (!inet_ntop(AF_INET6, &addr, str, sizeof(str))) + throw std::runtime_error("inet_ntop failed: errno"); + 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 + 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); + } + + /** + * 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 + 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; + + // Initialize once and reuse. + return helper; +} + 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; + std::map ret{}; - QString ps_output; - QString ps_output_err; - if (PowerShell::exec(ps_args, "Network Listing on Windows Platform", &ps_output, &ps_output_err)) + 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; + }; + + 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, 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, flags, 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) + const auto& adapter = *pitr; + + MIB_IF_ROW2 ifRow{}; + ifRow.InterfaceLuid = adapter.Luid; + if (GetIfEntry2(&ifRow) != NO_ERROR) + { + continue; + } + + // Only list the physical interfaces. + if (!ifRow.InterfaceAndOperStatusFlags.HardwareInterface) { - throw std::runtime_error{ - fmt::format("Could not determine available networks - unexpected powershell output: {}", - ps_output)}; + continue; } - auto iface = mp::NetworkInterfaceInfo{terms[0].toStdString(), - interpret_net_type(terms[1], terms[2]), - terms[3].toStdString()}; - ret.emplace(iface.id, iface); + 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)); } - return ret; + // 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 ({})", hyperv::maybe_widen{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; + } + } + } + } } - - 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 + { + throw GetNetworkInterfacesInfoException{"Failed to retrieve network interface information. Error code: {}", + result}; + } + return ret; } 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 +824,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 +870,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"); } @@ -816,11 +1075,13 @@ 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); + wchar_t username_buf[UNLEN + 1] = {}; + DWORD sz = sizeof(username_buf) / sizeof(wchar_t); + if (GetUserNameW(username_buf, &sz)) + { + return QString::fromWCharArray(username_buf, sz); + } + throw std::runtime_error("Failed retrieving user name!"); } QDir mp::platform::Platform::get_alias_scripts_folder() const 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}) 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/hyperv_api/CMakeLists.txt b/tests/hyperv_api/CMakeLists.txt index c55bc03c34..99e41c0b5b 100644 --- a/tests/hyperv_api/CMakeLists.txt +++ b/tests/hyperv_api/CMakeLists.txt @@ -19,8 +19,13 @@ 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_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/hyperv_test_utils.h b/tests/hyperv_api/hyperv_test_utils.h index f301aba5e1..e9abcad2a0 100644 --- a/tests/hyperv_api/hyperv_test_utils.h +++ b/tests/hyperv_api/hyperv_test_utils.h @@ -20,22 +20,29 @@ #include #include +#include + +#include #define EXPECT_NO_CALL(mock) EXPECT_CALL(mock, Call).Times(0) 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; } +/** + * 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 { @@ -60,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_bb_cit_hyperv.cpp b/tests/hyperv_api/test_bb_cit_hyperv.cpp index 1fa8740676..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 @@ -49,8 +54,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; }(); @@ -58,7 +63,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,6 +75,94 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) return create_disk_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 = [&network_adapter]() { + hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; + vm_parameters.name = "multipass-hyperv-cit-vm"; + vm_parameters.processor_count = 1; + vm_parameters.memory_size_mb = 512; + vm_parameters.network_adapters.push_back(network_adapter); + return vm_parameters; + }(); + + (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.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; + return network_parameters; + }(); + + const auto endpoint_parameters = [&network_parameters]() { + hyperv::hcn::CreateEndpointParameters endpoint_parameters{}; + endpoint_parameters.network_guid = network_parameters.guid; + endpoint_parameters.endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029f"; + return endpoint_parameters; + }(); + + // 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 create_vm_parameters = []() { hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; vm_parameters.name = "multipass-hyperv-cit-vm"; @@ -79,6 +171,13 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) 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)) @@ -96,14 +195,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); @@ -139,9 +230,13 @@ TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) ASSERT_TRUE(status_msg.empty()); } - // Add endpoint + // Add network adapter { - const auto& [status, status_msg] = hcs.add_endpoint(add_endpoint_parameters); + 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_it_hyperv_hcn_api.cpp b/tests/hyperv_api/test_it_hyperv_hcn_api.cpp index 947d2469aa..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,14 +61,13 @@ 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{}; 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_it_hyperv_hcs_api.cpp b/tests/hyperv_api/test_it_hyperv_hcs_api.cpp index 2a3753ab98..fc3465bf35 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,14 +40,18 @@ 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; 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)); + 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.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)); + 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) { @@ -62,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); @@ -88,7 +130,7 @@ TEST_F(HyperVHCSAPI_IntegrationTests, enumerate_properties) ASSERT_FALSE(d_result.status_msg.empty()); } -TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) +TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) { uut_t uut{}; @@ -97,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); @@ -113,15 +155,32 @@ TEST_F(HyperVHCSAPI_IntegrationTests, DISABLED_update_cpu_count) 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::format("{}", 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 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 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()); const auto d_result = uut.terminate_compute_system(params.name); ASSERT_TRUE(d_result); 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/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcn_api.cpp index fe44f67fbb..f168f63ed1 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. @@ -132,21 +135,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); @@ -156,8 +165,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_network_success) 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); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); *network = mock_network_object; }, Return(NOERROR))); @@ -171,11 +179,282 @@ 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.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + 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()); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); + *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()); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); + *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()); + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); + *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); @@ -223,11 +502,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.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + 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); @@ -277,18 +555,17 @@ 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"); } /****************************************************** * 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.subnet = "172.50.224.0/20"; - params.gateway = "172.50.224.1"; + 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); @@ -321,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 = hyperv::guid_to_string(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); }, @@ -435,11 +711,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ ], - "IpConfigurations": [ - { - "IpAddress": "172.50.224.27" - } - ] + "MacAddress": null })"""; ASSERT_NE(nullptr, network); @@ -451,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 = hyperv::guid_to_string(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))); @@ -463,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 = hyperv::guid_to_string(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); @@ -480,7 +750,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, @@ -493,10 +763,9 @@ 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"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [success, error_msg] = uut.create_endpoint(params); ASSERT_TRUE(success); @@ -529,7 +798,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, @@ -542,10 +811,9 @@ 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"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [status, error_msg] = uut.create_endpoint(params); ASSERT_FALSE(status); @@ -592,11 +860,7 @@ TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", "Policies": [ ], - "IpConfigurations": [ - { - "IpAddress": "172.50.224.27" - } - ] + "MacAddress": null })"""; ASSERT_EQ(mock_network_object, network); @@ -604,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 = hyperv::guid_to_string(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; }, @@ -617,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 = hyperv::guid_to_string(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; @@ -634,7 +896,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"); @@ -646,10 +908,9 @@ 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"; - params.endpoint_ipvx_addr = "172.50.224.27"; const auto& [success, error_msg] = uut.create_endpoint(params); ASSERT_FALSE(success); @@ -680,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 = hyperv::guid_to_string(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 new file mode 100644 index 0000000000..c98beeff99 --- /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::to_string(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::to_wstring(uut); + constexpr auto expected_result = LR"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..be50a6e873 --- /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::to_string(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::to_wstring(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..1e6f1d484c --- /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::to_string(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::to_wstring(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..608e98f32a --- /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::to_string(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::to_wstring(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 diff --git a/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/hyperv_api/test_ut_hyperv_hcs_api.cpp index 36effca062..b971d4f0cd 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" @@ -38,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(); @@ -219,7 +223,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -228,7 +232,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -236,8 +240,17 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) "ReadOnly": false } } - }, + } + }, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -299,12 +312,10 @@ 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) | 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, "HCSWrapper::create_compute_system(...) > params:"); + 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"); } /****************************************************** @@ -314,8 +325,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; @@ -385,7 +400,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) } }, "Scsi": { - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -393,8 +408,17 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) "ReadOnly": false } } - }, + } + }, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -456,12 +480,10 @@ 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) | 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, "HCSWrapper::create_compute_system(...) > params:"); + 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"); } /****************************************************** @@ -471,8 +493,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; @@ -542,7 +564,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -550,8 +572,17 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) "ReadOnly": true } } - }, + } + }, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -613,12 +644,10 @@ 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) | 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, "HCSWrapper::create_compute_system(...) > params:"); + 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"); } /****************************************************** @@ -628,8 +657,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; @@ -698,8 +729,16 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) "NamedPipe": "\\\\.\\pipe\\test_vm" } }, - "Scsi": { + "Scsi": {}, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} } } })"; @@ -761,12 +800,10 @@ 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) | 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, "HCSWrapper::create_compute_system(...) > params:"); + 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"); } /****************************************************** @@ -776,8 +813,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; @@ -817,7 +852,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(...)"); } /****************************************************** @@ -827,8 +862,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; @@ -892,7 +931,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -901,7 +940,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -909,9 +948,18 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) "ReadOnly": false } } - }, - } - } + } + }, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } } })"; @@ -954,7 +1002,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(...)"); } /****************************************************** @@ -964,8 +1012,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; @@ -1035,7 +1087,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) } }, "Scsi": { - "cloud-init iso file": { + "cloud-init": { "Attachments": { "0": { "Type": "Iso", @@ -1044,7 +1096,7 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) } } }, - "Primary disk": { + "primary": { "Attachments": { "0": { "Type": "VirtualDisk", @@ -1052,9 +1104,18 @@ TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) "ReadOnly": false } } - }, - } - } + } + }, + "NetworkAdapters": {}, + "Plan9": { + "Shares": [ + ] + } + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } } })"; @@ -1116,8 +1177,9 @@ 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::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"); } /****************************************************** @@ -1127,8 +1189,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; @@ -1405,9 +1471,10 @@ 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(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -1462,9 +1529,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!"); } @@ -1537,7 +1604,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!"); } @@ -1620,7 +1687,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!"); } @@ -1724,9 +1791,10 @@ 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(...) > ("); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "wait_for_operation_result(...) > finished"); } /****************************************************** @@ -1830,6 +1898,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) { @@ -1839,7 +1912,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); }); } @@ -1872,6 +1946,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) { @@ -1881,7 +1960,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); }); } @@ -1889,6 +1969,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) { @@ -1898,7 +1983,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); }); } @@ -2160,7 +2246,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"( { @@ -2176,15 +2262,15 @@ 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, "modify_compute_system(...) > 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"; + + 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); @@ -2197,35 +2283,39 @@ 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(...)"); - hyperv::hcs::AddEndpointParameters params{}; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsNetworkAdapter 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); }); } // --------------------------------------------------------- -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(...)"); - hyperv::hcs::AddEndpointParameters params{}; - params.target_compute_system_name = "test_vm"; - return wrapper.add_endpoint(params); + logger_scope.mock_logger->expect_log(mpl::Level::debug, "modify_compute_system(...)"); + hyperv::hcs::HcsNetworkAdapter 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); }); } // --------------------------------------------------------- -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"( { @@ -2241,12 +2331,14 @@ 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(...)"); - hyperv::hcs::AddEndpointParameters params{}; + 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.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"; + 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); @@ -2259,7 +2351,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"( { @@ -2275,12 +2367,14 @@ 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(...)"); - hyperv::hcs::AddEndpointParameters params{}; + 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.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"; + 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); @@ -2293,21 +2387,24 @@ 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"( { "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_endpoint(...) > name: (test_vm), endpoint_guid: (288cc1ac-8f31-4a09-9e90-30ad0bcfdbca)"); - return wrapper.remove_endpoint("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); @@ -2320,43 +2417,53 @@ 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, "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); }); } // --------------------------------------------------------- -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, "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); }); } // --------------------------------------------------------- -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"( { "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_endpoint(...)"); - return wrapper.remove_endpoint("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); @@ -2369,19 +2476,23 @@ 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"( { "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_endpoint(...)"); - return wrapper.remove_endpoint("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); @@ -2406,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); @@ -2426,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); }); } @@ -2438,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); }); } @@ -2457,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); @@ -2483,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); @@ -2597,21 +2723,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 +2746,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 +2789,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 +2806,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 +2829,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 +2844,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 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 diff --git a/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/hyperv_api/test_ut_hyperv_virtdisk.cpp index 0365eb6637..3a6a7f5973 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 @@ -38,60 +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_get_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_get_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. @@ -107,11 +105,9 @@ 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_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"); @@ -135,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)"); @@ -157,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. @@ -185,7 +171,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"); @@ -207,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)"); @@ -227,15 +212,129 @@ TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) // --------------------------------------------------------- -TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) +TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhdx_with_source) { + auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); + EXPECT_CALL(*mock_file_ops, exists(TypedEq(L"source.vhdx"))).WillOnce(Return(true)); + /****************************************************** - * Override the default mock functions. + * Verify that the dependencies are called with right + * data. ******************************************************/ - ::testing::MockFunction mock_create_virtual_disk; + { + 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_VHDX); + 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_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, 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, + VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN); - mock_api_table.CreateVirtualDisk = mock_create_virtual_disk.AsStdFunction(); + 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, + "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.predecessor = hyperv::virtdisk::SourcePathParameters{"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) +{ /****************************************************** * Verify that the dependencies are called with right * data. @@ -255,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)"); @@ -280,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. @@ -314,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))); @@ -335,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"); @@ -354,37 +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: 3"); } { @@ -400,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 @@ -433,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"); @@ -455,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. @@ -558,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"); } @@ -590,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. @@ -666,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"); @@ -694,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 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/mock_logger.cpp b/tests/mock_logger.cpp index 62054ed822..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); + 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) 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_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_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)); 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) { 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)