diff --git a/.clang-format b/.clang-format index 19cd344ad83..2ffba2d6f4c 100644 --- a/.clang-format +++ b/.clang-format @@ -39,6 +39,14 @@ PointerAlignment: Left --- Language: Cpp Standard: c++20 +IncludeCategories: + - Regex: '^$' + Priority: 100 + - Regex: '^$' + Priority: 200 + - Regex: '^$' + Priority: 300 + # "If none of the regular expressions match, INT_MAX is assigned as category." --- Language: ObjC --- diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 88ca83d0afc..6a0b6d29745 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -217,7 +217,7 @@ jobs: # we need to override it at the runner level. sudo bash -c 'echo "/coredump/%e.%p.%t" > /proc/sys/kernel/core_pattern' - - name: Test + - name: Run unit tests id: test if: ${{ matrix.build-type == 'Debug' }} timeout-minutes: 2 @@ -237,6 +237,26 @@ jobs: LD_LIBRARY_PATH=/root/stage/usr/lib/x86_64-linux-gnu/:/root/stage/lib/:/root/parts/multipass/build/lib/ \ /root/parts/multipass/build/bin/multipass_tests" + - name: Run integration tests + id: integration-tests + if: ${{ matrix.build-type == 'Debug' }} + timeout-minutes: 2 + run: | + + trap 'echo "MULTIPASS_TESTS_EXIT_CODE=$?" >> $GITHUB_ENV' EXIT + instance_name=`/snap/bin/lxc --project snapcraft --format=csv --columns=n list | grep multipass` + /snap/bin/lxc --project snapcraft start $instance_name | true + # Let's print the core pattern so we can check if it's successfully propagated to the container. + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c 'cat /proc/sys/kernel/core_pattern' + # Create the directory for the coredumps + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c 'mkdir -p /coredump' + # Enable coredumps by setting the core dump size to "unlimited", and run the tests. + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ + ulimit -c unlimited && \ + env CTEST_OUTPUT_ON_FAILURE=1 \ + LD_LIBRARY_PATH=/root/stage/usr/lib/x86_64-linux-gnu/:/root/stage/lib/:/root/parts/multipass/build/lib/ \ + /root/parts/multipass/build/bin/multipass_integration_tests" + - name: Measure coverage id: measure-coverage if: ${{ matrix.build-type == 'Coverage' }} diff --git a/.github/workflows/windows-macos.yml b/.github/workflows/windows-macos.yml index 27bc8239c45..13dfa10b2df 100644 --- a/.github/workflows/windows-macos.yml +++ b/.github/workflows/windows-macos.yml @@ -277,11 +277,16 @@ jobs: - name: Build run: cmake --build ${{ env.BUILD_DIR }} - - name: Test + - name: Run unit tests working-directory: ${{ env.BUILD_DIR }} run: | bin/multipass_tests + - name: Run integration tests + working-directory: ${{ env.BUILD_DIR }} + run: | + bin/multipass_integration_tests + - name: Package id: cmake-package working-directory: ${{ env.BUILD_DIR }} diff --git a/CMakeLists.txt b/CMakeLists.txt index c8f41942880..c750015b200 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,6 +231,9 @@ endif() # Boost config find_package(Boost CONFIG REQUIRED COMPONENTS json) +# Dependencies satisfied by CMake +include(src/cmake/cmake-deps.cmake) + # OpenSSL config find_package(OpenSSL CONFIG REQUIRED) @@ -283,7 +286,12 @@ if(MSVC) add_definitions(-DMULTIPASS_PLATFORM_WINDOWS) add_definitions(-D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS) add_definitions(-DWIN32_LEAN_AND_MEAN) + add_definitions(-DSECURITY_WIN32) set(MULTIPASS_BACKENDS hyperv virtualbox) + if(HYPERV_HCS_ENABLED) + add_definitions(-DHYPERV_HCS_ENABLED=1) + list(APPEND MULTIPASS_BACKENDS hyperv_api) + endif() set(MULTIPASS_PLATFORM windows) else() add_compile_options(-Werror -Wall -pedantic -fPIC) diff --git a/feature-flags.cmake b/feature-flags.cmake index 1bde03f2ae4..d53ec3bf638 100644 --- a/feature-flags.cmake +++ b/feature-flags.cmake @@ -16,3 +16,6 @@ include(src/cmake/feature-flag.cmake) # Multipass backend integrating with Apple Virtualization framework feature_flag(APPLEVZ_ENABLED "AppleVZ backend" APPLE) + +# The new Windows backend based on Hyper-V Host Compute System / Host Compute Networking APIs +feature_flag(HYPERV_HCS_ENABLED "Hyper-V HCS backend" WIN32) diff --git a/include/multipass/constants.h b/include/multipass/constants.h index a0089cf8115..7449ca60deb 100644 --- a/include/multipass/constants.h +++ b/include/multipass/constants.h @@ -75,4 +75,8 @@ constexpr auto petenv_default = "primary"; constexpr auto timeout_exit_code = 5; constexpr auto authenticated_certs_dir = "authenticated-certs"; constexpr auto home_in_instance = "/home/ubuntu"; + +constexpr std::chrono::milliseconds vm_shutdown_timeout = + 300000ms; // unit: ms, 5 minute timeout for shutdown/suspend +constexpr auto default_ssh_port = 22; } // namespace multipass diff --git a/include/multipass/exceptions/wsa_init_exception.h b/include/multipass/exceptions/wsa_init_exception.h new file mode 100644 index 00000000000..4669b0159b8 --- /dev/null +++ b/include/multipass/exceptions/wsa_init_exception.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass +{ + +struct WSAInitException : FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; +} // namespace multipass diff --git a/include/multipass/file_ops.h b/include/multipass/file_ops.h index 3d5d23977c2..1fe95772511 100644 --- a/include/multipass/file_ops.h +++ b/include/multipass/file_ops.h @@ -125,20 +125,34 @@ class FileOps : public Singleton const fs::path& dist, fs::copy_options copy_options, std::error_code& ec) const; + virtual void rename(const fs::path& old_p, const fs::path& new_p) const; virtual bool exists(const fs::path& path) const; - virtual bool exists(const fs::path& path, std::error_code& err) const; + virtual bool is_symlink(const fs::path& path) const; + // [[deprecated("Use non-std::error_code overload instead!")]] + virtual bool exists(const fs::path& path, std::error_code& err) const noexcept; + // [[deprecated("Use non-std::error_code overload instead!")]] virtual bool is_directory(const fs::path& path, std::error_code& err) const; + // [[deprecated("Use non-std::error_code overload instead!")]] virtual bool create_directory(const fs::path& path, std::error_code& err) const; + // [[deprecated("Use non-std::error_code overload instead!")]] 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; + // [[deprecated("Use non-std::error_code overload instead!")]] + virtual bool remove(const fs::path& path, std::error_code& err) const noexcept; + // [[deprecated("Use non-std::error_code overload instead!")]] virtual void create_symlink(const fs::path& to, const fs::path& path, std::error_code& err) const; + //[[deprecated("Use non-std::error_code overload instead!")]] virtual fs::path read_symlink(const fs::path& path, std::error_code& err) const; + //[[deprecated("Use non-std::error_code overload instead!")]] virtual fs::file_status status(const fs::path& path, std::error_code& err) const; + //[[deprecated("Use non-std::error_code overload instead!")]] virtual fs::file_status symlink_status(const fs::path& path, std::error_code& err) const; + //[[deprecated("Use non-std::error_code overload instead!")]] virtual std::unique_ptr recursive_dir_iterator(const fs::path& path, std::error_code& err) const; + //[[deprecated("Use non-std::error_code overload instead!")]] virtual std::unique_ptr dir_iterator(const fs::path& path, std::error_code& err) const; virtual fs::path weakly_canonical(const fs::path& path) const; diff --git a/include/multipass/network_interface.h b/include/multipass/network_interface.h index bba631c1cc7..66d914a7d05 100644 --- a/include/multipass/network_interface.h +++ b/include/multipass/network_interface.h @@ -42,11 +42,9 @@ struct NetworkInterface inline void tag_invoke(const boost::json::value_from_tag&, boost::json::value& json, - const NetworkInterface& interface) + const NetworkInterface& iface) { - json = {{"id", interface.id}, - {"mac_address", interface.mac_address}, - {"auto_mode", interface.auto_mode}}; + json = {{"id", iface.id}, {"mac_address", iface.mac_address}, {"auto_mode", iface.auto_mode}}; } inline NetworkInterface tag_invoke(const boost::json::value_to_tag&, diff --git a/include/multipass/signal.h b/include/multipass/signal.h index b2d65a46a3e..d11f6954a5b 100644 --- a/include/multipass/signal.h +++ b/include/multipass/signal.h @@ -28,13 +28,16 @@ struct Signal bool wait_for(const T& timeout) { std::unique_lock lock{mutex}; - return cv.wait_for(lock, timeout, [this] { return signaled; }); + const auto ret = cv.wait_for(lock, timeout, [this] { return signaled; }); + signaled = false; + return ret; } void wait() { std::unique_lock lock{mutex}; cv.wait(lock, [this] { return signaled; }); + signaled = false; } void signal() diff --git a/include/multipass/utils/static_bi_map.h b/include/multipass/utils/static_bi_map.h new file mode 100644 index 00000000000..86123bfd480 --- /dev/null +++ b/include/multipass/utils/static_bi_map.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 . + * + */ + +#pragma once + +#include + +#include + +namespace multipass +{ + +/** + * Inefficient, static bidirectional map using two std::unordered_map's. + */ +template +struct static_bi_map : private multipass::DisabledCopyMove +{ + static_bi_map(std::initializer_list> init) + : left(init), right([&init]() { + std::unordered_map map; + for (const auto& [k, v] : init) + map.emplace(v, k); + return map; + }()) + { + } + + const std::unordered_map left; + const std::unordered_map right; +}; + +} // namespace multipass diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 0e48318e300..ba7a290fb89 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -21,16 +21,17 @@ #include "network_interface.h" #include +#include +#include #include #include +#include #include #include #include #include -#include - namespace multipass { struct IPAddress; @@ -43,6 +44,7 @@ class Snapshot; class VirtualMachine : private DisabledCopyMove { public: + // TODO: Get rid of the VirtualMachine::State in favor of InstanceStatus enum class State { off, @@ -99,7 +101,8 @@ class VirtualMachine : private DisabledCopyMove 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; @@ -169,6 +172,9 @@ struct fmt::formatter : fmt::formatter::format(v, ctx); diff --git a/src/client/gui/lib/platform/windows.dart b/src/client/gui/lib/platform/windows.dart index 723527267dd..c7298972f45 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 00000000000..91241194f61 --- /dev/null +++ b/src/cmake/cmake-deps.cmake @@ -0,0 +1,25 @@ +# 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 out_ptr +# TODO: C++23: Remove this and use std::out_ptr instead. +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 703fdc818a4..d8b4a4f282a 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -30,7 +30,8 @@ function(add_target TARGET_NAME) shared_win scope_guard wineventlogger - OpenSSL::applink) + OpenSSL::applink + Secur32) elseif(APPLE) add_library(${TARGET_NAME} STATIC platform_osx.cpp diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp index 47f7dd8a1d1..dd4e38b129c 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp @@ -427,7 +427,7 @@ mp::VirtualMachine::State mp::HyperVVirtualMachine::current_state() int mp::HyperVVirtualMachine::ssh_port() { - return 22; + return default_ssh_port; } void mp::HyperVVirtualMachine::handle_state_update() diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.h b/src/platform/backends/hyperv/hyperv_virtual_machine.h index 1c58acbbc5b..c8887710860 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.h +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.h @@ -52,7 +52,7 @@ class HyperVVirtualMachine final : public BaseVirtualMachine const Path& dest_instance_dir); ~HyperVVirtualMachine(); void start() override; - void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override; + void shutdown(ShutdownPolicy shutdown_policy) override; void suspend() override; State current_state() override; int ssh_port() override; diff --git a/src/platform/backends/hyperv_api/CMakeLists.txt b/src/platform/backends/hyperv_api/CMakeLists.txt new file mode 100644 index 00000000000..1af47617b3c --- /dev/null +++ b/src/platform/backends/hyperv_api/CMakeLists.txt @@ -0,0 +1,82 @@ +# 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 . +# + +if(WIN32) + + include(CheckCXXSourceRuns) + + macro(check_pragma_lib LIB_NAME HEADER_NAME OUT_VAR) + check_cxx_source_runs(" + #pragma comment(lib, \"${LIB_NAME}\") + #define WIN32_LEAN_AND_MEAN + #include + #include <${HEADER_NAME}> + int main(void){ return 0; } + " ${OUT_VAR}) + endmacro() + + check_pragma_lib("computecore.lib" "computecore.h" HAS_COMPUTECORE) + check_pragma_lib("computenetwork.lib" "computenetwork.h" HAS_COMPUTENETWORK) + check_pragma_lib("virtdisk.lib" "virtdisk.h" HAS_VIRTDISK) + + if(NOT (HAS_COMPUTECORE AND HAS_COMPUTENETWORK AND HAS_VIRTDISK)) + message(FATAL_ERROR + "[hyperv_api] One or more required libraries are missing:\n" + " HAS_COMPUTECORE_LIB=${HAS_COMPUTECORE_LIB}\n" + " HAS_COMPUTENETWORK_LIB=${HAS_COMPUTENETWORK_LIB}\n" + " HAS_VIRTDISK_LIB=${HAS_VIRTDISK_LIB}\n" + ) + endif() + + add_library(hyperv_api_backend STATIC + hcn/hyperv_hcn_wrapper.cpp + hcn/hyperv_hcn_api.cpp + hcn/hyperv_hcn_route.cpp + hcn/hyperv_hcn_subnet.cpp + hcn/hyperv_hcn_ipam.cpp + hcn/hyperv_hcn_network_policy.cpp + hcn/hyperv_hcn_create_endpoint_params.cpp + hcn/hyperv_hcn_create_network_params.cpp + hcn/hyperv_hcn_endpoint_query.cpp + hcn/hyperv_hcn_network_info.cpp + hcs/hyperv_hcs_wrapper.cpp + hcs/hyperv_hcs_event_type.cpp + hcs/hyperv_hcs_scsi_device.cpp + hcs/hyperv_hcs_schema_version.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 + hcs/hyperv_hcs_api.cpp + virtdisk/virtdisk_wrapper.cpp + virtdisk/virtdisk_api.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 + Boost::json + platform + utils + scope_guard + computecore.lib + computenetwork.lib + virtdisk.lib + ) +endif() diff --git a/src/platform/backends/hyperv_api/format_as_mixin.h b/src/platform/backends/hyperv_api/format_as_mixin.h new file mode 100644 index 00000000000..884fb1c4d05 --- /dev/null +++ b/src/platform/backends/hyperv_api/format_as_mixin.h @@ -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 . + * + */ + +#pragma once + +#include +#include + +namespace multipass::hyperv +{ +/** + * Adds a free format_as function if the Derived is convertible to + * std::string_view. + * + * @tparam Derived Derived class to extend. + */ +template +struct FormatAsMixin +{ + friend constexpr std::string_view format_as(const Derived& v) noexcept + { + static_assert(std::is_convertible_v, + "Derived must be convertible to std::string_view"); + return v; + } +}; + +} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp new file mode 100644 index 00000000000..7a06434c94e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.cpp @@ -0,0 +1,87 @@ +/* + * 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 + +namespace multipass::hyperv::hcn +{ + +HCNAPI::HCNAPI(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton{pass} +{ +} + +HRESULT HCNAPI::HcnCreateNetwork(REFGUID Id, + PCWSTR Settings, + PHCN_NETWORK Network, + PWSTR* ErrorRecord) const +{ + return ::HcnCreateNetwork(Id, Settings, Network, ErrorRecord); +} +HRESULT HCNAPI::HcnOpenNetwork(REFGUID Id, PHCN_NETWORK Network, PWSTR* ErrorRecord) const +{ + return ::HcnOpenNetwork(Id, Network, ErrorRecord); +} +HRESULT HCNAPI::HcnDeleteNetwork(REFGUID Id, PWSTR* ErrorRecord) const +{ + return ::HcnDeleteNetwork(Id, ErrorRecord); +} +HRESULT HCNAPI::HcnCloseNetwork(HCN_NETWORK Network) const +{ + return ::HcnCloseNetwork(Network); +} +HRESULT HCNAPI::HcnCreateEndpoint(HCN_NETWORK Network, + REFGUID Id, + PCWSTR Settings, + PHCN_ENDPOINT Endpoint, + PWSTR* ErrorRecord) const +{ + return ::HcnCreateEndpoint(Network, Id, Settings, Endpoint, ErrorRecord); +} +HRESULT HCNAPI::HcnOpenEndpoint(REFGUID Id, PHCN_ENDPOINT Endpoint, PWSTR* ErrorRecord) const +{ + return ::HcnOpenEndpoint(Id, Endpoint, ErrorRecord); +} +HRESULT HCNAPI::HcnDeleteEndpoint(REFGUID Id, PWSTR* ErrorRecord) const +{ + return ::HcnDeleteEndpoint(Id, ErrorRecord); +} +HRESULT HCNAPI::HcnCloseEndpoint(HCN_ENDPOINT Endpoint) const +{ + return ::HcnCloseEndpoint(Endpoint); +} +HRESULT HCNAPI::HcnEnumerateEndpoints(PCWSTR Query, PWSTR* Endpoints, PWSTR* ErrorRecord) const +{ + return ::HcnEnumerateEndpoints(Query, Endpoints, ErrorRecord); +} +HRESULT HCNAPI::HcnEnumerateNetworks(PCWSTR Query, PWSTR* Networks, PWSTR* ErrorRecord) const +{ + return ::HcnEnumerateNetworks(Query, Networks, ErrorRecord); +} +HRESULT HCNAPI::HcnQueryNetworkProperties(HCN_NETWORK Network, + PCWSTR Query, + PWSTR* Properties, + PWSTR* ErrorRecord) const +{ + return ::HcnQueryNetworkProperties(Network, Query, Properties, ErrorRecord); +} +void HCNAPI::CoTaskMemFree(LPVOID pv) const +{ + ::CoTaskMemFree(pv); +} + +} // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.h new file mode 100644 index 00000000000..5712b85ca56 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_api.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 . + * + */ + +#pragma once + +#include + +#include +#include +#include // for CoTaskMemFree + +namespace multipass::hyperv::hcn +{ + +struct HCNAPI : public Singleton +{ + HCNAPI(const Singleton::PrivatePass&) noexcept; + + [[nodiscard]] virtual HRESULT HcnCreateNetwork(REFGUID Id, + PCWSTR Settings, + PHCN_NETWORK Network, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnOpenNetwork(REFGUID Id, + PHCN_NETWORK Network, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnDeleteNetwork(REFGUID Id, PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnCloseNetwork(HCN_NETWORK Network) const; + [[nodiscard]] virtual HRESULT HcnCreateEndpoint(HCN_NETWORK Network, + REFGUID Id, + PCWSTR Settings, + PHCN_ENDPOINT Endpoint, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnOpenEndpoint(REFGUID Id, + PHCN_ENDPOINT Endpoint, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnDeleteEndpoint(REFGUID Id, PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnCloseEndpoint(HCN_ENDPOINT Endpoint) const; + [[nodiscard]] virtual HRESULT HcnEnumerateEndpoints(PCWSTR Query, + PWSTR* Endpoints, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnEnumerateNetworks(PCWSTR Query, + PWSTR* Networks, + PWSTR* ErrorRecord) const; + [[nodiscard]] virtual HRESULT HcnQueryNetworkProperties(HCN_NETWORK Network, + PCWSTR Query, + PWSTR* Properties, + PWSTR* ErrorRecord) const; + + virtual void CoTaskMemFree(LPVOID pv) const; +}; + +} // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.cpp new file mode 100644 index 00000000000..d2a3d2322b8 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +namespace +{ +template +std::string value_or_null(const std::optional& opt) +{ + if (opt) + { + return fmt::format("\"{}\"", *opt); + } + return "null"; +} + +} // namespace + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const CreateEndpointParameters& params, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "SchemaVersion": {{ + "Major": 2, + "Minor": 16 + }}, + "HostComputeNetwork": "{0}", + "Policies": [], + "MacAddress" : {1} + }})json"); + + return json_template.format_to(ctx, params.network_guid, value_or_null(params.mac_address)); +} + +template auto fmt::formatter::format( + const CreateEndpointParameters&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const CreateEndpointParameters&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; 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 new file mode 100644 index 00000000000..363846fbc2f --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_endpoint_params.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Parameters for creating a network endpoint. + */ +struct CreateEndpointParameters +{ + /** + * The GUID of the network that will own the endpoint. + * + * The network must already exist. + */ + std::string network_guid{}; + + /** + * GUID for the new endpoint. + * + * Must be unique. + */ + std::string endpoint_guid{}; + + /** + * MAC address associated with the endpoint (optional). + * + * HCN will auto-assign a MAC address to the endpoint when + * not specified, where applicable. + */ + std::optional mac_address; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for CreateEndpointParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + + template + auto format(const multipass::hyperv::hcn::CreateEndpointParameters& params, + FormatContext& ctx) const -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.cpp new file mode 100644 index 00000000000..82ce62a9d5d --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.cpp @@ -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 . + * + */ + +#include +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const CreateNetworkParameters& params, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "SchemaVersion": + {{ + "Major": 2, + "Minor": 2 + }}, + "Name": "{0}", + "Type": "{1}", + "Ipams": [ + {2} + ], + "Flags": {3}, + "Policies": [ + {4} + ] + }} + )json"); + + return json_template.format_to(ctx, + params.name, + params.type, + fmt::join(params.ipams, string_literal(",")), + fmt::underlying(params.flags), + fmt::join(params.policies, string_literal(","))); +} + +template auto fmt::formatter::format( + const CreateNetworkParameters&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const CreateNetworkParameters&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; 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 new file mode 100644 index 00000000000..61d575f5576 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_create_network_params.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Parameters for creating a new Host Compute Network + */ +struct CreateNetworkParameters +{ + /** + * Name for the network + */ + 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{}; + + /** + * IP Address Management + */ + std::vector ipams{}; + + /** + * Network policies + */ + std::vector policies; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for CreateNetworkParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcn::CreateNetworkParameters& params, + FormatContext& ctx) const -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.cpp new file mode 100644 index 00000000000..6a34b3a6daa --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const EndpointQuery& params, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "SchemaVersion": + {{ + "Major": 2, + "Minor": 2 + }}, + "Filter": "{{\"VirtualMachine\": \"{0}\"}}" + }} + )json"); + + return json_template.format_to(ctx, params.vm_guid); +} + +template auto fmt::formatter::format( + const EndpointQuery&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const EndpointQuery&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.h new file mode 100644 index 00000000000..004d762f7b7 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_endpoint_query.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 . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Parameters for creating a network endpoint. + */ +struct EndpointQuery +{ + /** + * The GUID of the VM that endpoint belongs to. Note that it's not the VM's name -- the name + * needs to be resolved to a GUID via `get_compute_system_guid`. + */ + std::string vm_guid{}; +}; + +} // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for EndpointQuery + */ +template +struct fmt::formatter + : formatter, Char> +{ + + template + auto format(const multipass::hyperv::hcn::EndpointQuery& params, FormatContext& ctx) const + -> FormatContext::iterator; +}; 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 00000000000..d7ea42e41cf --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const HcnIpam& ipam, FormatContext& ctx) const + -> FormatContext::iterator +{ + constexpr static auto json_template = string_literal(R"json( + {{ + "Type": "{}", + "Subnets": [ + {} + ] + }} + )json"); + + return json_template.format_to(ctx, + ipam.type, + fmt::join(ipam.subnets, string_literal(","))); +} + +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 00000000000..cef93210b01 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Hcn IP Address Management structure + */ +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 + -> FormatContext::iterator; +}; 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 00000000000..1da0d6e5805 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h @@ -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 . + * + */ + +#pragma once + +#include + +#include + +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnIpamType : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + [[nodiscard]] static HcnIpamType Dhcp() + { + return {"DHCP"}; + } + + [[nodiscard]] static HcnIpamType Static() + { + return {"static"}; + } + +private: + HcnIpamType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn 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 00000000000..8c953f1476a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_flags.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +#include +#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, ///< ?? +}; + +[[nodiscard]] 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 + : formatter, Char> +{ + 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 fmt::format_to(ctx.out(), "{}", fmt::join(parts, " | ")); + } +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.cpp new file mode 100644 index 00000000000..93d821f3823 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.cpp @@ -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 . + * + */ + +#include +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const HcnNetworkInfo& info, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "Id": "{0}", + "Name": "{1}", + "Type": "{2}" + }} + )json"); + + return json_template.format_to(ctx, info.guid, info.name, info.type); +} + +template auto fmt::formatter::format( + const HcnNetworkInfo&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const HcnNetworkInfo&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.h new file mode 100644 index 00000000000..afaaac633c9 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_info.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 . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcn +{ +struct HcnNetworkInfo +{ + std::string guid; + std::string name; + std::string type; + std::optional network_adapter_name; +}; + +}; // namespace multipass::hyperv::hcn + +/** + * Formatter type specialization for HcnNetworkInfo + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcn::HcnNetworkInfo& policy, FormatContext& ctx) const + -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.cpp new file mode 100644 index 00000000000..768ac6ebc95 --- /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 namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +struct NetworkPolicySettingsFormatters +{ + auto operator()(const HcnNetworkPolicyNetAdapterName& policy) const + { + static constexpr auto json_template = string_literal(R"json( + "NetworkAdapterName": "{}" + )json"); + + return json_template.format(policy.net_adapter_name); + } +}; + +template +template +auto fmt::formatter::format(const HcnNetworkPolicy& policy, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "Type": "{}", + "Settings": {{ + {} + }} + }} + )json"); + + return json_template.format_to( + ctx, + 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 00000000000..3c14523e64d --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#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 + -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h new file mode 100644 index 00000000000..bdfdb6c0d34 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_netadaptername.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass::hyperv::hcn +{ + +struct HcnNetworkPolicyNetAdapterName +{ + std::string net_adapter_name; +}; + +} // namespace multipass::hyperv::hcn 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 00000000000..552cbc78aa7 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_policy_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 . + * + */ + +#pragma once + +#include + +#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 : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + /** + * @since Version 2.0 + */ + [[nodiscard]] static HcnNetworkPolicyType NetAdapterName() + { + return {"NetAdapterName"}; + } + + [[nodiscard]] bool operator==(const HcnNetworkPolicyType& rhs) const + { + return value == rhs.value; + } + +private: + HcnNetworkPolicyType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn 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 00000000000..ee7a37b2ba5 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_network_type.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcn +{ + +/** + * Strongly-typed string values for network type. + */ +struct HcnNetworkType : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + /** + * @since Version 2.0 + */ + [[nodiscard]] static HcnNetworkType Nat() + { + return {"NAT"}; + } + + /** + * @since Version 2.0 + */ + [[nodiscard]] static HcnNetworkType Ics() + { + return {"ICS"}; + } + + /** + * @since Version 2.0 + */ + [[nodiscard]] static HcnNetworkType Transparent() + { + return {"Transparent"}; + } + + /** + * @since Version 2.0 + */ + [[nodiscard]] static HcnNetworkType L2Bridge() + { + return {"L2Bridge"}; + } + + [[nodiscard]] bool operator==(const HcnNetworkType& rhs) const + { + return value == rhs.value; + } + +private: + constexpr HcnNetworkType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcn 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 00000000000..7229dbcdfc0 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.cpp @@ -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 . + * + */ + +#include + +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const HcnRoute& route, FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "NextHop": "{}", + "DestinationPrefix": "{}", + "Metric": {} + }})json"); + + return json_template.format_to(ctx, route.next_hop, 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 00000000000..99aeccf30a2 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_route.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 . + * + */ + +#pragma once + +#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 + -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.cpp new file mode 100644 index 00000000000..f768abd7b5a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.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 namespace multipass::hyperv; +using namespace multipass::hyperv::hcn; + +template +template +auto fmt::formatter::format(const HcnSubnet& subnet, FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "Policies": [], + "Routes" : [ + {} + ], + "IpAddressPrefix" : "{}", + "IpSubnets": null + }} + )json"); + + return json_template.format_to(ctx, + fmt::join(subnet.routes, string_literal(",")), + 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 00000000000..d46fbeacf36 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_subnet.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 . + * + */ + +#pragma once + +#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 + -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp new file mode 100644 index 00000000000..c686f63bd70 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.cpp @@ -0,0 +1,372 @@ +/* + * 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 + +#include +#include +#include + +using ztd::out_ptr::out_ptr; + +namespace multipass::hyperv::hcn +{ + +struct GuidParseError : multipass::FormattedExceptionBase<> +{ + using FormattedExceptionBase<>::FormattedExceptionBase; +}; + +namespace +{ +inline const HCNAPI& API() +{ + return HCNAPI::instance(); +} + +struct HcnNetworkCloser +{ + void operator()(HCN_NETWORK p) const noexcept + { + (void)API().HcnCloseNetwork(p); + } +}; + +using UniqueHcnNetwork = std::unique_ptr, HcnNetworkCloser>; + +struct HcnEndpointCloser +{ + void operator()(HCN_ENDPOINT p) const noexcept + { + (void)API().HcnCloseEndpoint(p); + } +}; + +using UniqueHcnEndpoint = std::unique_ptr, HcnEndpointCloser>; + +struct CotaskmemStringDeleter +{ + void operator()(void* p) const noexcept + { + API().CoTaskMemFree(p); + } +}; +using UniqueCotaskmemString = std::unique_ptr; + +namespace mpl = logging; +using lvl = mpl::Level; + +// --------------------------------------------------------- + +constexpr auto log_category = "HyperV-HCN-Wrapper"; + +// --------------------------------------------------------- + +/** + * Parse given GUID string into a GUID struct. + * + * @param guid_wstr GUID in wide 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 guid_length = 36; + constexpr auto guid_length_with_braces = guid_length + 2; + + const auto input = [&guid_wstr]() { + switch (guid_wstr.length()) + { + case guid_length: + // CLSIDFromString requires GUIDs to be wrapped with braces. + return fmt::format(L"{{{}}}", guid_wstr); + case guid_length_with_braces: + { + if (guid_wstr.front() != L'{' || guid_wstr.back() != 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_str) -> ::GUID +{ + const std::wstring v = to_wstring(guid_str); + return guid_from_string(v); +} + +// --------------------------------------------------------- + +template +OperationResult perform_hcn_operation(const FnType& fn) +{ + 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, if + // it occurred. Therefore, we can streamline all API calls through perform_hcn_operation. + const auto result = ResultCode{fn(out_ptr(result_msgbuf))}; + + mpl::trace(log_category, "perform_hcn_operation(...) > result: {}", result.success()); + + // Avoid null to be forward-compatible with C++23 + return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; +} + +// --------------------------------------------------------- + +std::pair open_network(const std::string& network_guid) +{ + mpl::trace(log_category, "open_network(...) > network_guid: {} ", network_guid); + + UniqueHcnNetwork network{}; + const auto result = perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnOpenNetwork(guid_from_string(network_guid), out_ptr(network), rmsgbuf); + }); + + return std::make_pair(result, std::move(network)); +} + +} // namespace + +// --------------------------------------------------------- + +HCNWrapper::HCNWrapper(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton{pass} +{ +} + +// --------------------------------------------------------- + +OperationResult HCNWrapper::create_network(const CreateNetworkParameters& params) const +{ + mpl::trace(log_category, "HCNWrapper::create_network(...) > params: {} ", params); + + UniqueHcnNetwork network{}; + const auto network_settings = fmt::to_wstring(params); + + return perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnCreateNetwork(guid_from_string(params.guid), + network_settings.c_str(), + out_ptr(network), + rmsgbuf); + }); +} + +// --------------------------------------------------------- + +OperationResult HCNWrapper::delete_network(const std::string& network_guid) const +{ + mpl::trace(log_category, "HCNWrapper::delete_network(...) > network_guid: {}", network_guid); + + return perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnDeleteNetwork(guid_from_string(network_guid), rmsgbuf); + }); +} + +// --------------------------------------------------------- + +OperationResult HCNWrapper::create_endpoint(const CreateEndpointParameters& params) const +{ + mpl::trace(log_category, "HCNWrapper::create_endpoint(...) > params: {} ", params); + + const auto& [open_network_result, network] = open_network(params.network_guid); + + if (nullptr == network) + { + return open_network_result; + } + + UniqueHcnEndpoint endpoint{}; + const auto params_json = fmt::to_wstring(params); + + return perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnCreateEndpoint(network.get(), + guid_from_string(params.endpoint_guid), + params_json.c_str(), + out_ptr(endpoint), + rmsgbuf); + }); +} + +// --------------------------------------------------------- + +OperationResult HCNWrapper::delete_endpoint(const std::string& endpoint_guid) const +{ + mpl::trace(log_category, + "HCNWrapper::delete_endpoint(...) > endpoint_guid: {} ", + endpoint_guid); + + return perform_hcn_operation([&](auto&& rmsgbuf) { + return API().HcnDeleteEndpoint(guid_from_string(endpoint_guid), rmsgbuf); + }); +} + +// --------------------------------------------------------- + +OperationResult HCNWrapper::enumerate_attached_endpoints( + const std::string& vm_guid, + std::vector& endpoint_guids) const +{ + mpl::trace(log_category, + "HCNWrapper::enumerate_attached_endpoints(...) > vm_guid: {} ", + vm_guid); + + const auto query = EndpointQuery{.vm_guid = vm_guid}; + const auto query_wstring = fmt::to_wstring(query); + + UniqueCotaskmemString json_output{}, result_msgbuf{}; + + const auto result = API().HcnEnumerateEndpoints(query_wstring.c_str(), + out_ptr(json_output), + out_ptr(result_msgbuf)); + + if (json_output) + { + const auto endpoints_as_str = wchar_to_utf8(json_output.get()); + std::error_code ec; + const auto as_json = boost::json::parse(endpoints_as_str, ec); + if (!ec) + { + const auto& as_array = as_json.as_array(); + for (const auto& elem : as_array) + { + endpoint_guids.emplace_back(elem.as_string()); + } + } + } + + return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; +} + +OperationResult HCNWrapper::query_network(const std::string& network_guid, + HcnNetworkInfo& out_info) const +{ + mpl::trace(log_category, "HCNWrapper::query_network(...) > network_guid: {}", network_guid); + + if (const auto& [open_network_result, network] = open_network(network_guid); + open_network_result) + { + UniqueCotaskmemString query_result{}, result_msgbuf{}; + const auto result = API().HcnQueryNetworkProperties(network.get(), + L"{}", + out_ptr(query_result), + out_ptr(result_msgbuf)); + if (query_result) + { + const auto json_as_str = wchar_to_utf8(query_result.get()); + std::error_code ec; + const auto as_json = boost::json::parse(json_as_str, ec); + mpl::trace(log_category, "query_network result: {}", json_as_str); + if (!ec) + { + const auto& obj = as_json.as_object(); + out_info.guid = network_guid; + out_info.name = obj.at("Name").as_string(); + + if (const auto* value = obj.if_contains("NetworkAdapterName")) + out_info.network_adapter_name = value->as_string(); + + if (const auto* value = obj.if_contains("Type")) + out_info.type = value->as_string(); + else + { + // Quoting hcsshim: + // "If HNS sets the network type to NAT (i.e. '0' in + // HNS.Schema.Network.NetworkMode),the value will be omitted from the JSON blob. + // We therefore need to initializeNAT here before unmarshaling the JSON blob." + out_info.type = "NAT"; + } + } + } + return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; + } + else + return open_network_result; +} + +OperationResult HCNWrapper::enumerate_networks(std::vector& out_network_guids) const +{ + mpl::trace(log_category, "HCNWrapper::enumerate_networks(...)"); + + UniqueCotaskmemString enumerate_result{}, result_msgbuf{}; + + // List all HCN network GUIDs + const auto result = + API().HcnEnumerateNetworks(L"{}", out_ptr(enumerate_result), out_ptr(result_msgbuf)); + if (enumerate_result) + { + // json_output would contain the network GUIDs. + const auto json_as_str = wchar_to_utf8(enumerate_result.get()); + std::error_code ec; + const auto as_json = boost::json::parse(json_as_str, ec); + if (!ec) + { + for (const auto& network_guid : as_json.as_array()) + { + out_network_guids.emplace_back(std::string{network_guid.as_string()}); + } + } + } + return {result, {result_msgbuf ? result_msgbuf.get() : L""}}; +} +} // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h new file mode 100644 index 00000000000..11a634f934b --- /dev/null +++ b/src/platform/backends/hyperv_api/hcn/hyperv_hcn_wrapper.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include + +namespace multipass::hyperv::hcn +{ + +/** + * A high-level wrapper class that defines the common operations that Host Compute Network API + * provide. + */ +struct HCNWrapper : public Singleton +{ + + HCNWrapper(const Singleton::PrivatePass&) noexcept; + [[nodiscard]] virtual OperationResult create_network( + const CreateNetworkParameters& params) const; + [[nodiscard]] virtual OperationResult delete_network(const std::string& network_guid) const; + [[nodiscard]] virtual OperationResult create_endpoint( + const CreateEndpointParameters& params) const; + [[nodiscard]] virtual OperationResult delete_endpoint(const std::string& endpoint_guid) const; + [[nodiscard]] virtual OperationResult enumerate_attached_endpoints( + const std::string& vm_guid, + std::vector& endpoint_guids) const; + [[nodiscard]] virtual OperationResult enumerate_networks( + std::vector& network_guids) const; + [[nodiscard]] virtual OperationResult query_network(const std::string& network_guid, + HcnNetworkInfo& out_info) const; +}; + +inline const HCNWrapper& HCN() +{ + return HCNWrapper::instance(); +} + +} // namespace multipass::hyperv::hcn diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.cpp new file mode 100644 index 00000000000..e6cd951b085 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.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 + +namespace multipass::hyperv::hcs +{ + +HCSAPI::HCSAPI(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton{pass} +{ +} + +HCS_OPERATION HCSAPI::HcsCreateOperation(const void* context, + HCS_OPERATION_COMPLETION callback) const +{ + return ::HcsCreateOperation(context, callback); +} + +HRESULT HCSAPI::HcsWaitForOperationResult(HCS_OPERATION operation, + DWORD timeoutMs, + PWSTR* resultDocument) const +{ + return ::HcsWaitForOperationResult(operation, timeoutMs, resultDocument); +} + +void HCSAPI::HcsCloseOperation(HCS_OPERATION operation) const +{ + return ::HcsCloseOperation(operation); +} + +HRESULT HCSAPI::HcsCreateComputeSystem(PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) const +{ + return ::HcsCreateComputeSystem(id, + configuration, + operation, + securityDescriptor, + computeSystem); +} + +HRESULT HCSAPI::HcsOpenComputeSystem(PCWSTR id, + DWORD requestedAccess, + HCS_SYSTEM* computeSystem) const +{ + return ::HcsOpenComputeSystem(id, requestedAccess, computeSystem); +} + +HRESULT HCSAPI::HcsStartComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsStartComputeSystem(computeSystem, operation, options); +} + +HRESULT HCSAPI::HcsShutDownComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsShutDownComputeSystem(computeSystem, operation, options); +} + +HRESULT HCSAPI::HcsTerminateComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsTerminateComputeSystem(computeSystem, operation, options); +} + +void HCSAPI::HcsCloseComputeSystem(HCS_SYSTEM computeSystem) const +{ + return ::HcsCloseComputeSystem(computeSystem); +} + +HRESULT HCSAPI::HcsPauseComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsPauseComputeSystem(computeSystem, operation, options); +} + +HRESULT HCSAPI::HcsResumeComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsResumeComputeSystem(computeSystem, operation, options); +} + +HRESULT HCSAPI::HcsModifyComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) const +{ + return ::HcsModifyComputeSystem(computeSystem, operation, configuration, identity); +} + +HRESULT HCSAPI::HcsGetComputeSystemProperties(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR propertyQuery) const +{ + return ::HcsGetComputeSystemProperties(computeSystem, operation, propertyQuery); +} + +HRESULT HCSAPI::HcsGrantVmAccess(PCWSTR vmId, PCWSTR filePath) const +{ + return ::HcsGrantVmAccess(vmId, filePath); +} + +HRESULT HCSAPI::HcsRevokeVmAccess(PCWSTR vmId, PCWSTR filePath) const +{ + return ::HcsRevokeVmAccess(vmId, filePath); +} + +HRESULT HCSAPI::HcsEnumerateComputeSystems(PCWSTR query, HCS_OPERATION operation) const +{ + return ::HcsEnumerateComputeSystems(query, operation); +} + +HRESULT HCSAPI::HcsSetComputeSystemCallback(HCS_SYSTEM computeSystem, + HCS_EVENT_OPTIONS callbackOptions, + const void* context, + HCS_EVENT_CALLBACK callback) const +{ + return ::HcsSetComputeSystemCallback(computeSystem, callbackOptions, context, callback); +} + +HRESULT HCSAPI::HcsSaveComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const +{ + return ::HcsSaveComputeSystem(computeSystem, operation, options); +} + +HRESULT HCSAPI::HcsCreateEmptyGuestStateFile(PCWSTR guestStateFilePath) const +{ + return ::HcsCreateEmptyGuestStateFile(guestStateFilePath); +} + +HRESULT HCSAPI::HcsCreateEmptyRuntimeStateFile(PCWSTR runtimeStateFilePath) const +{ + return ::HcsCreateEmptyRuntimeStateFile(runtimeStateFilePath); +} + +HLOCAL HCSAPI::LocalFree(HLOCAL hMem) const +{ + return ::LocalFree(hMem); +} + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.h new file mode 100644 index 00000000000..bc85984e3dc --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_api.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HCSAPI : public Singleton +{ + HCSAPI(const Singleton::PrivatePass&) noexcept; + + [[nodiscard]] virtual HCS_OPERATION HcsCreateOperation(const void* context, + HCS_OPERATION_COMPLETION callback) const; + [[nodiscard]] virtual HRESULT HcsWaitForOperationResult(HCS_OPERATION operation, + DWORD timeoutMs, + PWSTR* resultDocument) const; + virtual void HcsCloseOperation(HCS_OPERATION operation) const; + [[nodiscard]] virtual HRESULT HcsCreateComputeSystem( + PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) const; + [[nodiscard]] virtual HRESULT HcsOpenComputeSystem(PCWSTR id, + DWORD requestedAccess, + HCS_SYSTEM* computeSystem) const; + [[nodiscard]] virtual HRESULT HcsStartComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + [[nodiscard]] virtual HRESULT HcsShutDownComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + [[nodiscard]] virtual HRESULT HcsTerminateComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + virtual void HcsCloseComputeSystem(HCS_SYSTEM computeSystem) const; + [[nodiscard]] virtual HRESULT HcsPauseComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + [[nodiscard]] virtual HRESULT HcsResumeComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + [[nodiscard]] virtual HRESULT HcsModifyComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) const; + [[nodiscard]] virtual HRESULT HcsGetComputeSystemProperties(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR propertyQuery) const; + [[nodiscard]] virtual HRESULT HcsGrantVmAccess(PCWSTR vmId, PCWSTR filePath) const; + [[nodiscard]] virtual HRESULT HcsRevokeVmAccess(PCWSTR vmId, PCWSTR filePath) const; + [[nodiscard]] virtual HRESULT HcsEnumerateComputeSystems(PCWSTR query, + HCS_OPERATION operation) const; + [[nodiscard]] virtual HRESULT HcsSetComputeSystemCallback(HCS_SYSTEM computeSystem, + HCS_EVENT_OPTIONS callbackOptions, + const void* context, + HCS_EVENT_CALLBACK callback) const; + [[nodiscard]] virtual HRESULT HcsSaveComputeSystem(HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR options) const; + [[nodiscard]] virtual HRESULT HcsCreateEmptyGuestStateFile(PCWSTR guestStateFilePath) const; + [[nodiscard]] virtual HRESULT HcsCreateEmptyRuntimeStateFile(PCWSTR runtimeStateFilePath) const; + + virtual HLOCAL LocalFree(HLOCAL hMem) const; +}; + +} // namespace multipass::hyperv::hcs 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 new file mode 100644 index 00000000000..94141ad01cd --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_compute_system_state.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +namespace multipass::hyperv::hcs +{ + +/** + * Enum values representing a compute system's possible state + * + * @ref https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#State + */ +enum class ComputeSystemState : std::uint8_t +{ + created, + running, + paused, + stopped, + saved_as_template, + unknown, +}; + +namespace detail +{ +[[nodiscard]] inline const auto& compute_system_state_map() +{ + static const static_bi_map state_map{ + {"created", ComputeSystemState::created}, + {"running", ComputeSystemState::running}, + {"paused", ComputeSystemState::paused}, + {"stopped", ComputeSystemState::stopped}, + {"savedastemplate", ComputeSystemState::saved_as_template}, + {"unknown", ComputeSystemState::unknown}, + }; + return state_map; +} +} // namespace detail + +/** + * Translate host compute system state string to enum + * + * @param str + * @return ComputeSystemState + */ +[[nodiscard]] 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); + }); + + const auto& map = detail::compute_system_state_map().left; + + if (const auto itr = map.find(str); map.end() != itr) + return itr->second; + + return std::nullopt; +} + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(multipass::hyperv::hcs::ComputeSystemState state, FormatContext& ctx) const + { + std::string_view v = "(undefined)"; + const auto& map = multipass::hyperv::hcs::detail::compute_system_state_map().right; + + if (const auto itr = map.find(state); map.end() != itr) + v = itr->second; + + return fmt::format_to(ctx.out(), "{}", v); + } +}; 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 00000000000..5a87f0219c4 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.cpp @@ -0,0 +1,169 @@ +/* + * 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 namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +namespace +{ +template +std::string value_or_null(const std::optional& opt) +{ + if (opt) + { + return fmt::format("\"{}\"", *opt); + } + return "null"; +} + +void append_if(auto& target, bool condition, auto&& callable) +{ + if (condition) + { + if (target.empty()) + { + // To emit an initial comma. + target.push_back({}); + } + target.emplace_back(typename std::decay_t::value_type{callable()}); + } +} + +} // namespace + +template +template +auto fmt::formatter::format( + const CreateComputeSystemParameters& params, + FormatContext& ctx) const -> FormatContext::iterator +{ + static constexpr auto json_template = string_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": {0} + }}, + "Processor": {{ + "Count": {1} + }} + }}, + "Devices": {{ + "ComPorts": {{ + "0": {{ + "NamedPipe": "\\\\.\\pipe\\{2}" + }} + }}, + "Scsi": {{ + {3} + }}, + "NetworkAdapters": {{ + {4} + }} + {5} + }} + {6} + }} + }} + )json"); + + std::vector> optional_sections{}, optional_devices{}; + + append_if(optional_sections, + SchemaUtils::instance().get_os_supported_schema_version() >= HcsSchemaVersion::v25, + [] { + return string_literal(R"json( + "Services": { + "Shutdown": {}, + "Heartbeat": {} + })json"); + }); + + append_if(optional_sections, + params.guest_state.guest_state_file_path.has_value() || + params.guest_state.runtime_state_file_path.has_value(), + [&vmgs = params.guest_state.guest_state_file_path, + &vmrs = params.guest_state.runtime_state_file_path] { + return string_literal(R"json( + "GuestState": {{ + "GuestStateFilePath": {0}, + "RuntimeStateFilePath": {1} + }} + )json") + .format(value_or_null(vmgs), value_or_null(vmrs)); + }); + + append_if(optional_sections, + params.guest_state.save_state_file_path.has_value(), + [&save_state = params.guest_state.save_state_file_path] { + return string_literal(R"json( + "RestoreState": {{ + "SaveStateFilePath": "{0}" + }} + )json") + .format(*save_state); + }); + + // append_if(optional_devices, !params.shares.empty(), [&shares = params.shares] { + // // Had to extract it bc an empty shares array causes a vmwp.exe crash while saving the VM + // return string_literal(R"json( + // "Plan9": {{ + // "Shares": [ + // {0} + // ] + // }})json") + // .format(fmt::join(shares, string_literal(","))); + // }); + + return json_template.format_to(ctx, + params.memory_size_mb, + params.processor_count, + params.name, + fmt::join(params.scsi_devices, string_literal(",")), + fmt::join(params.network_adapters, string_literal(",")), + fmt::join(optional_devices, string_literal(",")), + fmt::join(optional_sections, string_literal(","))); +} + +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 new file mode 100644 index 00000000000..5a7c9d2f3d7 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace multipass::hyperv::hcs +{ + +struct GuestState +{ + std::optional guest_state_file_path{}; + std::optional runtime_state_file_path{}; + std::optional save_state_file_path{}; +}; + +struct CreateComputeSystemParameters +{ + /** + * Unique name for the compute system + */ + std::string name{}; + + /** + * Memory size, in megabytes + */ + std::uint32_t memory_size_mb{}; + + /** + * vCPU count + */ + std::uint32_t processor_count{}; + + /** + * List of SCSI devices that are attached on boot + */ + std::vector scsi_devices{}; + + /** + * List of endpoints that'll be added to the compute system + * by default at creation time. + */ + std::vector network_adapters{}; + + /** + * List of Plan9 shares that'll be added to the compute system + * by default at creation time. + */ + std::vector shares{}; + + /** + * Guest & runtime state file paths, if any. + */ + GuestState guest_state{}; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::CreateComputeSystemParameters& policy, + FormatContext& ctx) const -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.cpp new file mode 100644 index 00000000000..13e940f47c3 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include + +namespace multipass::hyperv::hcs +{ + +HcsEventType parse_event(const HCS_EVENT* hcs_event) +{ + switch (hcs_event->Type) + { + case HcsEventSystemExited: + return HcsEventType::SystemExited; + default: + return HcsEventType::Unknown; + } +} +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.h new file mode 100644 index 00000000000..d24d499827a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_event_type.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +struct HCS_EVENT; + +namespace multipass::hyperv::hcs +{ + +enum class HcsEventType +{ + Unknown, + SystemExited +}; + +[[nodiscard]] HcsEventType parse_event(const HCS_EVENT* hcs_event); +} // namespace multipass::hyperv::hcs 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 00000000000..86440ea434e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_modify_memory_settings.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsModifyMemorySettings +{ + std::uint32_t size_in_mb{0}; +}; + +} // namespace multipass::hyperv::hcs 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 00000000000..0bfb8af8ab2 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.cpp @@ -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 . + * + */ +#include + +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +template +template +auto fmt::formatter::format(const HcsNetworkAdapter& network_adapter, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + "{0}": {{ + "EndpointId" : "{0}", + "MacAddress": "{1}", + "InstanceId": "{0}" + }} + )json"); + + return json_template.format_to(ctx, network_adapter.endpoint_guid, 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 00000000000..6ab91ec050a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_network_adapter.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsNetworkAdapter +{ + 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 + -> FormatContext::iterator; +}; 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 00000000000..dc4b1f1a30f --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.cpp @@ -0,0 +1,43 @@ +/* + * 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 + -> FormatContext::iterator +{ + if constexpr (std::is_same_v) + { + return fmt::format_to(ctx.out(), "{}", path.get().generic_string()); + } + else if constexpr (std::is_same_v) + { + return fmt::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 00000000000..65d658011e9 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_path.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 . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcs +{ +/** + * The Host Compute System API expects paths with a single forward slash. HcsPath is a strong type + * that ensures the correct formatting. + */ +struct HcsPath +{ + template + requires std::constructible_from + HcsPath(Args&&... arg) : value{std::forward(arg)...} + { + } + + template + HcsPath& operator=(T&& v) + { + value = std::forward(v); + return *this; + } + [[nodiscard]] 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 + -> FormatContext::iterator; +}; 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 00000000000..032baaab977 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.cpp @@ -0,0 +1,81 @@ +/* + * 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 namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +template +template +auto fmt::formatter::format( + const HcsAddPlan9ShareParameters& params, + FormatContext& ctx) const -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "Name": "{0}", + "Path": "{1}", + "Port": {2}, + "AccessName": "{3}", + "Flags": {4} + }} + )json"); + + return json_template.format_to(ctx, + params.name, + params.host_path, + params.port, + params.access_name, + fmt::underlying(params.flags)); +} + +template +template +auto fmt::formatter::format( + const HcsRemovePlan9ShareParameters& params, + FormatContext& ctx) const -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "Name": "{0}", + "AccessName": "{1}", + "Port": {2} + }} + )json"); + + return json_template.format_to(ctx, params.name, 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 00000000000..e48b30fae2e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_plan9_share_params.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#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 constexpr std::uint16_t default_port{55035}; + + /** + * Unique name for the share + */ + std::string name{}; + + /** + * The name by which the guest operating 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{}; + + 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 -> 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 -> FormatContext::iterator; +}; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp new file mode 100644 index 00000000000..a5559de313e --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.cpp @@ -0,0 +1,103 @@ +/* + * 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 namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +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&) const + { + return std::basic_string(string_literal("null")); + } + + auto operator()(const HcsNetworkAdapter& params) const + { + static constexpr auto json_template = string_literal(R"json( + {{ + "EndpointId": "{0}", + "MacAddress": "{1}", + "InstanceId": "{0}" + }} + )json"); + + return json_template.format(params.endpoint_guid, params.mac_address); + } + + auto operator()(const HcsModifyMemorySettings& params) const + { + return to_string(params.size_in_mb); + } + + auto operator()(const HcsAddPlan9ShareParameters& params) const + { + return to_string(params); + } + + auto operator()(const HcsRemovePlan9ShareParameters& params) const + { + return to_string(params); + } +}; + +template +template +auto fmt::formatter::format(const HcsRequest& param, FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + {{ + "ResourcePath": "{0}", + "RequestType": "{1}", + "Settings": {2} + }} + )json"); + + return json_template.format_to( + ctx, + param.resource_path, + 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 00000000000..36f3ebda0de --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#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 + -> FormatContext::iterator; +}; 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 00000000000..67bbaacb143 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_request_type.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 . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsRequestType : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + [[nodiscard]] static HcsRequestType Add() + { + return {"Add"}; + } + + [[nodiscard]] static HcsRequestType Remove() + { + return {"Remove"}; + } + + [[nodiscard]] static HcsRequestType Update() + { + return {"Update"}; + } + + [[nodiscard]] bool operator==(const HcsRequestType& rhs) const + { + return value == rhs.value; + } + +private: + HcsRequestType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.h new file mode 100644 index 00000000000..2bbdb211f37 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_resource_path.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 . + * + */ + +#pragma once + +#include + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsResourcePath : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + [[nodiscard]] static HcsResourcePath NetworkAdapters(const std::string& network_adapter_id) + { + return fmt::format("VirtualMachine/Devices/NetworkAdapters/{{{0}}}", network_adapter_id); + } + + [[nodiscard]] static HcsResourcePath Memory() + { + return {"VirtualMachine/ComputeTopology/Memory/SizeInMB"}; + } + + [[nodiscard]] static HcsResourcePath Plan9Shares() + { + return {"VirtualMachine/Devices/Plan9/Shares"}; + } + + [[nodiscard]] bool operator==(const HcsResourcePath& rhs) const + { + return value == rhs.value; + } + +private: + HcsResourcePath(std::string v) : value(std::move(v)) + { + } + + std::string value{}; +}; + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.cpp new file mode 100644 index 00000000000..6a1e3c1e24d --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.cpp @@ -0,0 +1,159 @@ +/* + * 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 + +namespace +{ + +// https://www.wikiwand.com/en/articles/Windows_10_version_history +// https://www.wikiwand.com/en/articles/Windows_11_version_history +// https://www.wikiwand.com/en/articles/List_of_Microsoft_Windows_versions +enum class WindowsBuildNumbers : std::uint32_t +{ + // April 2018 Update, April 30, 2018 + win10_1809 = 17763, + // May 2019 Update, May 21, 2019 + win10_19H1 = 18362, + // May 2020 Update, May 27, 2020 + win10_20H1 = 19041, + // Codename "Vibranium", August 18, 2021 + srv22_21H2 = 20348, + // Codename "Sun Valley", October 5, 2021 + win11_21H2 = 22000 +}; + +struct SchemaVersionBuildNumberMapping +{ + multipass::hyperv::hcs::HcsSchemaVersion version; + WindowsBuildNumbers required_build_number; + + static bool descending(const SchemaVersionBuildNumberMapping& lhs, + const SchemaVersionBuildNumberMapping& rhs) + { + if (lhs.required_build_number != rhs.required_build_number) + return lhs.required_build_number > rhs.required_build_number; + + return lhs.version > rhs.version; + } +}; +} // namespace + +namespace multipass::hyperv::hcs +{ + +SchemaUtils::SchemaUtils(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton{pass} +{ +} + +// --------------------------------------------------------- + +HcsSchemaVersion SchemaUtils::get_os_supported_schema_version() const +{ + const static auto cached_schema_version = []() -> std::optional { + if (const auto winver = get_windows_version()) + { + + std::array schema_version_mappings{ + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v20, + WindowsBuildNumbers::win10_1809}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v21, + WindowsBuildNumbers::win10_1809}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v22, + WindowsBuildNumbers::win10_19H1}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v23, + WindowsBuildNumbers::win10_19H1}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v24, + WindowsBuildNumbers::srv22_21H2}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v25, + WindowsBuildNumbers::srv22_21H2}, + SchemaVersionBuildNumberMapping{HcsSchemaVersion::v26, + WindowsBuildNumbers::win11_21H2}}; + + // Sort descending, based on build number and version (when build number is equal) + std::sort(schema_version_mappings.begin(), + schema_version_mappings.end(), + SchemaVersionBuildNumberMapping::descending); + + const auto it = std::ranges::find_if(schema_version_mappings, [winver](const auto& m) { + return fmt::underlying(m.required_build_number) <= winver->build; + }); + + if (it != schema_version_mappings.end()) + return it->version; + } + return {}; + }(); + + // If unable to determine default to the lowest possible schema version + return cached_schema_version.value_or(HcsSchemaVersion::v20); +} +} // namespace multipass::hyperv::hcs + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +template +template +auto fmt::formatter::format(const HcsSchemaVersion& schema_version, + FormatContext& ctx) const + -> FormatContext::iterator +{ + + std::basic_string_view result = string_literal("Unknown"); + + switch (schema_version) + { + case HcsSchemaVersion::v20: + result = string_literal("v2.0"); + break; + case HcsSchemaVersion::v21: + result = string_literal("v2.1"); + break; + case HcsSchemaVersion::v22: + result = string_literal("v2.2"); + break; + case HcsSchemaVersion::v23: + result = string_literal("v2.3"); + break; + case HcsSchemaVersion::v24: + result = string_literal("v2.4"); + break; + case HcsSchemaVersion::v25: + result = string_literal("v2.5"); + break; + case HcsSchemaVersion::v26: + result = string_literal("v2.6"); + break; + } + + return string_literal(R"json({})json").format_to(ctx, result); +} + +template auto fmt::formatter::format( + const HcsSchemaVersion&, + fmt::format_context&) const -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const HcsSchemaVersion&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.h new file mode 100644 index 00000000000..dfb82135b81 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_schema_version.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +/** + * Host Compute System schema versions + * @ref https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#Schema-Version-Map + */ +enum class HcsSchemaVersion +{ + // Windows 10 SDK, version 1809 (10.0.17763.0) + v20 = 20, + // Windows 10 SDK, version 1809 (10.0.17763.0) + v21 = 21, + // Windows 10 SDK, version 1903 (10.0.18362.1) + v22 = 22, + // Windows 10 SDK, version 2004 (10.0.19041.0) + v23 = 23, + // Windows Server 2022 (OS build 20348.169) + v24 = 24, + // Windows Server 2022 (OS build 20348.169) + v25 = 25, + // Windows 11 SDK, version 21H2 (10.0.22000.194) + v26 = 26 +}; + +struct SchemaUtils : public Singleton +{ + SchemaUtils(const Singleton::PrivatePass&) noexcept; + // --------------------------------------------------------- + + /** + * Retrieve the supported HCS schema version by the host. + */ + [[nodiscard]] virtual HcsSchemaVersion get_os_supported_schema_version() const; +}; + +} // namespace multipass::hyperv::hcs + +/** + * Formatter type specialization for HcsRequest + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::hcs::HcsSchemaVersion& param, FormatContext& ctx) const + -> FormatContext::iterator; +}; 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 00000000000..d4a600bd564 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.cpp @@ -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 . + * + */ +#include + +#include + +#include + +using namespace multipass::hyperv; +using namespace multipass::hyperv::hcs; + +template +template +auto fmt::formatter::format(const HcsScsiDevice& scsi_device, + FormatContext& ctx) const + -> FormatContext::iterator +{ + static constexpr auto json_template = string_literal(R"json( + "{0}": {{ + "Attachments": {{ + "0": {{ + "Type": "{1}", + "Path": "{2}", + "ReadOnly": {3} + }} + }} + }} + )json"); + + return json_template.format_to(ctx, + scsi_device.name, + 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 00000000000..6a77f37e017 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device.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 . + * + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace multipass::hyperv::hcs +{ + +struct HcsScsiDevice +{ + HcsScsiDeviceType type; + std::string name; + HcsPath path; + bool read_only{false}; +}; + +} // 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 + -> FormatContext::iterator; +}; 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 00000000000..424759de474 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_scsi_device_type.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +#include + +namespace multipass::hyperv::hcs +{ + +/** + * Strongly-typed string values for SCSI device type. + */ +struct HcsScsiDeviceType : FormatAsMixin +{ + [[nodiscard]] operator std::string_view() const + { + return value; + } + + [[nodiscard]] static HcsScsiDeviceType Iso() + { + return {"Iso"}; + } + + [[nodiscard]] static HcsScsiDeviceType VirtualDisk() + { + return {"VirtualDisk"}; + } + + [[nodiscard]] bool operator==(const HcsScsiDeviceType& rhs) const + { + return value == rhs.value; + } + +private: + HcsScsiDeviceType(std::string_view v) : value(v) + { + } + + std::string_view value{}; +}; + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_system_handle.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_system_handle.h new file mode 100644 index 00000000000..cc7601f2147 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_system_handle.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass::hyperv::hcs +{ +using HcsSystemHandle = std::shared_ptr; +} diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.cpp b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.cpp new file mode 100644 index 00000000000..45058327055 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.cpp @@ -0,0 +1,593 @@ +/* + * 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 + +using ztd::out_ptr::out_ptr; + +namespace multipass::hyperv::hcs +{ + +namespace +{ + +inline const HCSAPI& API() +{ + return HCSAPI::instance(); +} + +struct HcsSystemCloser +{ + void operator()(HCS_SYSTEM p) const noexcept + { + API().HcsCloseComputeSystem(p); + } +}; + +using UniqueHcsSystem = std::unique_ptr, HcsSystemCloser>; + +struct HcsOperationCloser +{ + void operator()(HCS_OPERATION p) const noexcept + { + API().HcsCloseOperation(p); + } +}; + +using UniqueHcsOperation = + std::unique_ptr, HcsOperationCloser>; + +struct HlocalStringDeleter +{ + void operator()(void* p) const noexcept + { + API().LocalFree(p); + } +}; +using UniqueHlocalString = std::unique_ptr; + +namespace mpl = logging; +using lvl = mpl::Level; + +constexpr auto log_category = "HyperV-HCS-Wrapper"; +constexpr auto default_operation_timeout = std::chrono::seconds{240}; + +// --------------------------------------------------------- + +UniqueHcsOperation create_operation() +{ + mpl::trace(log_category, "create_operation(...)"); + return UniqueHcsOperation{API().HcsCreateOperation(nullptr, nullptr)}; +} + +// --------------------------------------------------------- + +OperationResult wait_for_operation_result( + UniqueHcsOperation op, + std::chrono::milliseconds timeout = default_operation_timeout) +{ + mpl::debug(log_category, + "wait_for_operation_result(...) > ({}), timeout: {} ms", + fmt::ptr(op.get()), + timeout.count()); + + UniqueHlocalString result_msg{}; + const auto hresult_code = + ResultCode{API().HcsWaitForOperationResult(op.get(), timeout.count(), out_ptr(result_msg))}; + mpl::debug(log_category, + "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; +} + +// --------------------------------------------------------- + +template +OperationResult perform_hcs_operation(const FnType& fn, const HcsSystemHandle& system) +{ + + if (nullptr == system) + { + mpl::error(log_category, + "perform_hcs_operation(...) > Host Compute System handle is null!"); + return OperationResult{E_POINTER, L"HcsCreateOperation failed!"}; + } + + auto operation = create_operation(); + + if (nullptr == operation) + { + mpl::error(log_category, "perform_hcs_operation(...) > HcsCreateOperation failed!"); + return OperationResult{E_POINTER, L"HcsCreateOperation failed!"}; + } + + // Perform the operation. + const auto result = ResultCode{fn(operation.get())}; + + if (!result.success()) + { + mpl::error(log_category, + "perform_hcs_operation(...) > Operation failed! Result code {}", + result); + return OperationResult{result, L"HCS operation failed!"}; + } + + mpl::debug(log_category, "perform_hcs_operation(...) > result: {}", result.success()); + + return wait_for_operation_result(std::move(operation)); +} + +} // namespace + +// --------------------------------------------------------- + +HCSWrapper::HCSWrapper(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton{pass} +{ +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::open_compute_system(const std::string& name, + HcsSystemHandle& out_hcs_system) const +{ + mpl::debug(log_category, "open_compute_system(...) > name: ({})", name); + + // Windows API uses wide strings. + const std::wstring name_w = to_wstring(name); + constexpr auto requested_access_level = GENERIC_ALL; + + UniqueHcsSystem system{}; + const ResultCode result = + API().HcsOpenComputeSystem(name_w.c_str(), requested_access_level, out_ptr(system)); + if (!result.success()) + { + mpl::debug(log_category, + "open_compute_system(...) > failed to open ({}), result code: ({})", + name, + result); + } + + out_hcs_system = std::move(system); + + return {result, L""}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::create_compute_system(const CreateComputeSystemParameters& params, + HcsSystemHandle& out_hcs_system) const +{ + mpl::debug(log_category, "HCSWrapper::create_compute_system(...) > params: {} ", params); + + // Initialize guest state files if they're absent + { + auto&& vmgs = params.guest_state.guest_state_file_path; + auto&& vmrs = params.guest_state.runtime_state_file_path; + + if (vmgs && !std::filesystem::exists(vmgs->get())) + { + if (const auto r = HCS().create_empty_guest_state_file(params.name, vmgs->get()); !r) + return r; + } + + if (vmrs && !std::filesystem::exists(vmrs->get())) + { + if (const auto r = HCS().create_empty_runtime_state_file(params.name, vmrs->get()); !r) + return r; + } + } + + const std::wstring name_w = to_wstring(params.name); + // Render the template + const auto vm_settings = fmt::to_wstring(params); + + auto operation = create_operation(); + + if (nullptr == operation) + { + return OperationResult{E_POINTER, L"HcsCreateOperation failed."}; + } + + UniqueHcsSystem system{}; + const auto result = ResultCode{API().HcsCreateComputeSystem(name_w.c_str(), + vm_settings.c_str(), + operation.get(), + nullptr, + out_ptr(system))}; + + if (!result.success()) + { + return OperationResult{result, L"HcsCreateComputeSystem failed."}; + } + + const auto op_result = + wait_for_operation_result(std::move(operation), std::chrono::seconds{240}); + + if (op_result) + { + out_hcs_system = std::move(system); + } + + return op_result; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::start_compute_system(const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "start_compute_system(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsStartComputeSystem(static_cast(target_hcs_system.get()), + op, + nullptr); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::shutdown_compute_system(const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "shutdown_compute_system(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + static constexpr wchar_t shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsShutDownComputeSystem(static_cast(target_hcs_system.get()), + op, + shutdown_option); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::terminate_compute_system(const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "terminate_compute_system(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsTerminateComputeSystem(static_cast(target_hcs_system.get()), + op, + nullptr); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::pause_compute_system(const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "pause_compute_system(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + static constexpr wchar_t pause_option[] = LR"( + { + "SuspensionLevel": "Suspend", + "HostedNotification": { + "Reason": "Save" + } + })"; + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsPauseComputeSystem(static_cast(target_hcs_system.get()), + op, + pause_option); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::resume_compute_system(const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "resume_compute_system(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsResumeComputeSystem(static_cast(target_hcs_system.get()), + op, + nullptr); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::get_compute_system_properties( + const HcsSystemHandle& target_hcs_system) const +{ + mpl::debug(log_category, + "get_compute_system_properties(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + // https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#System_PropertyType + static constexpr wchar_t vm_query[] = LR"( + { + "PropertyTypes":[] + })"; + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsGetComputeSystemProperties( + static_cast(target_hcs_system.get()), + op, + vm_query); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::grant_vm_access(const std::string& compute_system_name, + const std::filesystem::path& file_path) const +{ + mpl::debug(log_category, + "grant_vm_access(...) > name: ({}), file_path: ({})", + compute_system_name, + file_path.string()); + + const auto path_as_wstring = file_path.generic_wstring(); + const std::wstring csname_as_wstring = to_wstring(compute_system_name); + const auto result = API().HcsGrantVmAccess(csname_as_wstring.c_str(), path_as_wstring.c_str()); + return {result, FAILED(result) ? L"GrantVmAccess failed!" : L""}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::revoke_vm_access(const std::string& compute_system_name, + const std::filesystem::path& file_path) const +{ + mpl::debug(log_category, + "revoke_vm_access(...) > name: ({}), file_path: ({}) ", + compute_system_name, + file_path.string()); + + const auto path_as_wstring = file_path.wstring(); + const std::wstring csname_as_wstring = to_wstring(compute_system_name); + const auto result = API().HcsRevokeVmAccess(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 HcsSystemHandle& target_hcs_system, + ComputeSystemState& state_out) const +{ + mpl::debug(log_category, + "get_compute_system_state(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + const auto result = perform_hcs_operation( + [&](auto&& op) { + return API().HcsGetComputeSystemProperties( + static_cast(target_hcs_system.get()), + op, + nullptr); + }, + target_hcs_system); + + if (!result) + return result; + + 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::get_compute_system_guid(const HcsSystemHandle& target_hcs_system, + std::string& guid_out) const +{ + mpl::debug(log_category, + "get_compute_system_guid(...) > handle: ({})", + fmt::ptr(target_hcs_system.get())); + + const auto result = perform_hcs_operation( + [&](auto&& op) { + return API().HcsGetComputeSystemProperties( + static_cast(target_hcs_system.get()), + op, + nullptr); + }, + target_hcs_system); + + if (!result) + return result; + + const std::string result_msg_str = wchar_to_utf8(result.status_msg); + + std::error_code ec; + const auto parsed = boost::json::parse(result_msg_str, ec); + if (ec) + { + return {E_FAIL, L"Json parse error"}; + } + + const auto json_object = parsed.as_object(); + + if (const auto it = json_object.find("RuntimeId"); it != json_object.end()) + { + guid_out = it->value().as_string(); + return result; + } + return {E_FAIL, L"GUID not found in compute system properties"}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::modify_compute_system(const HcsSystemHandle& target_hcs_system, + const HcsRequest& params) const +{ + mpl::debug(log_category, + "modify_compute_system(...) > handle: ({}), params: {}", + fmt::ptr(target_hcs_system.get()), + params); + + const auto json = fmt::to_wstring(params); + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsModifyComputeSystem(static_cast(target_hcs_system.get()), + op, + json.c_str(), + nullptr); + }, + target_hcs_system); +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::set_compute_system_callback(const HcsSystemHandle& target_hcs_system, + void* context, + void (*callback)(HCS_EVENT* hcs_event, + void* context)) const +{ + mpl::debug(log_category, + "set_compute_system_callback(...) > handle: {}, context: {}, callback: {}", + fmt::ptr(target_hcs_system.get()), + fmt::ptr(context), + fmt::ptr(callback)); + + const ResultCode result = + API().HcsSetComputeSystemCallback(static_cast(target_hcs_system.get()), + HCS_EVENT_OPTIONS::HcsEventOptionNone, + context, + reinterpret_cast(callback)); + return {result, L""}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::save_compute_system(const HcsSystemHandle& target_hcs_system, + const HcsPath& save_path) const +{ + mpl::debug(log_category, + "save_compute_system(...) > handle: {}, save_path: {}", + fmt::ptr(target_hcs_system.get()), + save_path); + + static constexpr auto json_template = string_literal(R"( + {{ + "SaveType": "ToFile", + "SaveStateFilePath": "{0}" + }})"); + + const auto save_option = json_template.format(save_path); + + return perform_hcs_operation( + [&](auto&& op) { + return API().HcsSaveComputeSystem(static_cast(target_hcs_system.get()), + op, + save_option.c_str()); + }, + target_hcs_system); +} + +// --------------------------------------------------------- +OperationResult HCSWrapper::create_empty_guest_state_file( + const std::string& compute_system_name, + const std::filesystem::path& vmgs_file_path) const +{ + const std::wstring path_w = vmgs_file_path.generic_wstring(); + const auto result = ResultCode{API().HcsCreateEmptyGuestStateFile(path_w.c_str())}; + if (result.success()) + { + return grant_vm_access(compute_system_name, vmgs_file_path); + } + + return {result, L""}; +} + +// --------------------------------------------------------- + +OperationResult HCSWrapper::create_empty_runtime_state_file( + const std::string& compute_system_name, + const std::filesystem::path& vmrs_file_path) const +{ + const std::wstring path_w = vmrs_file_path.generic_wstring(); + const auto result = ResultCode{API().HcsCreateEmptyRuntimeStateFile(path_w.c_str())}; + if (result.success()) + { + return grant_vm_access(compute_system_name, vmrs_file_path); + } + return {result, L""}; +} + +} // namespace multipass::hyperv::hcs diff --git a/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.h b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.h new file mode 100644 index 00000000000..c41788e9c92 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs/hyperv_hcs_wrapper.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +struct HCS_EVENT; + +namespace multipass::hyperv::hcs +{ + +/** + * A high-level wrapper class that defines the common operations that Host Compute System API + * provide. + */ +struct HCSWrapper : public Singleton +{ + HCSWrapper(const Singleton::PrivatePass&) noexcept; + [[nodiscard]] virtual OperationResult open_compute_system( + const std::string& compute_system_name, + HcsSystemHandle& out_hcs_system) const; + [[nodiscard]] virtual OperationResult create_compute_system( + const CreateComputeSystemParameters& params, + HcsSystemHandle& out_hcs_system) const; + [[nodiscard]] virtual OperationResult start_compute_system( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult shutdown_compute_system( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult terminate_compute_system( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult pause_compute_system( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult resume_compute_system( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult + save_compute_system(const HcsSystemHandle& target_hcs_system, const HcsPath& save_path) const; + [[nodiscard]] virtual OperationResult get_compute_system_properties( + const HcsSystemHandle& target_hcs_system) const; + [[nodiscard]] virtual OperationResult grant_vm_access( + const std::string& compute_system_name, + const std::filesystem::path& file_path) const; + [[nodiscard]] virtual OperationResult revoke_vm_access( + const std::string& compute_system_name, + const std::filesystem::path& file_path) const; + [[nodiscard]] virtual OperationResult create_empty_guest_state_file( + const std::string& compute_system_name, + const std::filesystem::path& vmgs_file_path) const; + [[nodiscard]] virtual OperationResult create_empty_runtime_state_file( + const std::string& compute_system_name, + const std::filesystem::path& vmrs_file_path) const; + [[nodiscard]] virtual OperationResult get_compute_system_state( + const HcsSystemHandle& target_hcs_system, + ComputeSystemState& state_out) const; + [[nodiscard]] virtual OperationResult + get_compute_system_guid(const HcsSystemHandle& target_hcs_system, std::string& guid_out) const; + [[nodiscard]] virtual OperationResult modify_compute_system( + const HcsSystemHandle& target_hcs_system, + const HcsRequest& request) const; + [[nodiscard]] virtual OperationResult set_compute_system_callback( + const HcsSystemHandle& target_hcs_system, + void* context, + void (*callback)(HCS_EVENT* hcs_event, void* context)) const; +}; + +inline const HCSWrapper& HCS() +{ + return HCSWrapper::instance(); +} + +} // 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 00000000000..d19eaceecd8 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.cpp @@ -0,0 +1,188 @@ +/* + * 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 + +namespace multipass::hyperv::hcs +{ + +namespace mpu = utils; + +constexpr auto log_category = "hcs-plan9-mount-handler"; + +Plan9MountHandler::Plan9MountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + VMMount mount_spec, + const std::string& target) + : MountHandler(vm, ssh_key_provider, std::move(mount_spec), target) +{ + // No need to do anything special. + 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] { + 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 HcsRequest{HcsResourcePath::Plan9Shares(), HcsRequestType::Add(), params}; + }(); + + HcsSystemHandle handle{nullptr}; + if (!HCS().open_compute_system(vm->get_name(), handle)) + { + throw std::runtime_error{"Could not open Host Compute System for the mount"}; + } + + const auto result = HCS().modify_compute_system(handle, 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::debug(log_category, + "{}(): `id -u` = {}", + std::source_location::current(), + default_uid); + const auto default_gid = std::stoi(MP_UTILS.run_in_ssh_session(session, "id -g")); + mpl::debug(log_category, + "{}(): `id -g` = {}", + std::source_location::current(), + default_gid); + + mpu::make_target_dir(session, leading, missing); + mpu::set_owner_for(session, leading, missing, default_uid, default_gid); + } + + 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(log_category, + "Successfully mounted 9P share `{}` to VM `{}`", + req, + vm->get_name()); + } + else + { + mpl::error(log_category, + "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 remove_share_request = [this] { + 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 HcsRequest{HcsResourcePath::Plan9Shares(), HcsRequestType::Remove(), params}; + }(); + if (!HCS().modify_compute_system(handle, remove_share_request)) + { + mpl::warn(log_category, "Could not remove Plan9 share after activation failure."); + } + } +} +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 (auto exec_result = session.exec(umount_command); exec_result.exit_code() != 0) + { + mpl::warn(log_category, + "Plan9 share unmount failed. stdout: {0}, stderr: {1}", + exec_result.read_std_output(), + exec_result.read_std_error()); + + if (!force) + { + return; + } + } + + const auto req = [this] { + 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 HcsRequest{HcsResourcePath::Plan9Shares(), HcsRequestType::Remove(), params}; + }(); + + HcsSystemHandle handle{nullptr}; + if (!HCS().open_compute_system(vm->get_name(), handle)) + { + throw std::runtime_error{"Could not open Host Compute System for the unmount"}; + } + + if (!HCS().modify_compute_system(handle, req)) + { + mpl::warn(log_category, "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 00000000000..203efd4e0f2 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_plan9_mount_handler.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass::hyperv::hcs +{ + +class Plan9MountHandler : public MountHandler +{ +public: + Plan9MountHandler(VirtualMachine* vm, + const SSHKeyProvider* ssh_key_provider, + VMMount mount_spec, + const std::string& target); + + ~Plan9MountHandler() override; + +private: + void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; + void deactivate_impl(bool force) override; +}; + +} // namespace multipass::hyperv::hcs 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 00000000000..a4b3c0be811 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -0,0 +1,750 @@ +/* + * 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 +#include + +#include + +#include + +namespace +{ + +namespace mp = multipass; +namespace mpl = mp::logging; +using mp::hyperv::hcn::HCN; +using mp::hyperv::hcs::HCS; +using mp::hyperv::virtdisk::VirtDisk; +using namespace mp::hyperv; + +inline auto mac2uuid(std::string mac_addr) +{ + std::erase(mac_addr, ':'); + std::erase(mac_addr, '-'); + constexpr auto format_str = "db4bdbf0-dc14-407f-9780-{}"; + return fmt::format(format_str, mac_addr); +} + +inline auto replace_colon_with_dash(const std::string& addr) +{ + if (addr.empty()) + return addr; + std::string result{addr}; + std::ranges::replace(result, ':', '-'); + return result; +} + +/** + * Perform a DNS resolve of @p hostname to obtain IPv4/IPv6 + * address(es) associated with it. + * + * @param [in] hostname Hostname to resolve + * @return Vector of IPv4/IPv6 addresses + */ +auto resolve_ip_addresses(const std::string& hostname) +{ + const static mp::wsa_init_wrapper wsa_context{}; + + std::vector ipv4{}, ipv6{}; + mpl::trace("resolve-ip-addr", + "resolve_ip_addresses() -> resolve being called for hostname `{}`", + hostname); + + // Wrap the raw addrinfo pointer so it's always destroyed properly. + const auto& [result, addr_info] = [&]() { + struct addrinfo* result = {nullptr}; + // clang-format off + // (xmkg): different behavior between clang-format versions. + struct addrinfo hints + { + + }; + // clang-format on + const auto r = getaddrinfo(hostname.c_str(), nullptr, nullptr, &result); + return std::make_pair( + r, + std::unique_ptr{result, freeaddrinfo}); + }(); + + if (result == 0) + { + assert(addr_info.get()); + for (auto ptr = addr_info.get(); ptr != nullptr; ptr = addr_info->ai_next) + { + switch (ptr->ai_family) + { + case AF_INET: + { + constexpr auto sockaddr_in_size = sizeof(std::remove_pointer_t); + if (ptr->ai_addrlen >= sockaddr_in_size) + { + const auto sockaddr_ipv4 = reinterpret_cast(ptr->ai_addr); + char addr[INET_ADDRSTRLEN] = {}; + inet_ntop(AF_INET, &(sockaddr_ipv4->sin_addr), addr, sizeof(addr)); + ipv4.push_back(addr); + break; + } + + mpl::error("resolve-ip-addr", + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv4 address " + "data while expecting {}!", + ptr->ai_addrlen, + sockaddr_in_size); + } + break; + case AF_INET6: + { + constexpr auto sockaddr_in6_size = sizeof(std::remove_pointer_t); + if (ptr->ai_addrlen >= sockaddr_in6_size) + { + const auto sockaddr_ipv6 = reinterpret_cast(ptr->ai_addr); + char addr[INET6_ADDRSTRLEN] = {}; + inet_ntop(AF_INET6, &(sockaddr_ipv6->sin6_addr), addr, sizeof(addr)); + ipv6.push_back(addr); + break; + } + mpl::error("resolve-ip-addr", + "resolve_ip_addresses() -> anomaly: received {} bytes of IPv6 address " + "data while expecting {}!", + ptr->ai_addrlen, + sockaddr_in6_size); + } + break; + default: + continue; + } + } + } + + mpl::trace("resolve-ip-addr", + "resolve_ip_addresses() -> hostname: {} resolved to : (v4: {}, v6: {})", + hostname, + fmt::join(ipv4, ","), + fmt::join(ipv6, ",")); + + return std::make_pair(ipv4, ipv6); +} + +void try_create_endpoints( + const std::string& vm_name, + const std::vector& create_endpoint_params) +{ + std::vector created_endpoints; + for (const auto& endpoint : create_endpoint_params) + { + if (HCN().delete_endpoint(endpoint.endpoint_guid)) + { + mpl::warn(vm_name, + "Endpoint {} was already present, removed it.", + endpoint.endpoint_guid); + } + if (const auto result = HCN().create_endpoint(endpoint)) + created_endpoints.push_back(endpoint); + else + { + for (const auto& created_ep : created_endpoints) + { + mpl::warn(vm_name, + "Removing endpoint {} due to failed operation, result {}", + created_ep.endpoint_guid, + HCN().delete_endpoint(created_ep.endpoint_guid)); + } + throw multipass::hyperv::CreateEndpointException{ + "create_endpoint failed with {}, endpoint details: {}", + result.code, + endpoint}; + } + } +} + +} // namespace + +namespace multipass::hyperv +{ + +HCSVirtualMachine::HCSVirtualMachine(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), + monitor(monitor) +{ + const auto created_from_scratch = maybe_create_compute_system(); + const auto state = fetch_state_from_api(); + + mpl::debug(get_name(), + "HCSVirtualMachine::HCSVirtualMachine() > `{}`, created_from_scratch: {}, state: {}", + get_name(), + created_from_scratch, + state); + + // Reflect compute system's state + set_state(state); + HCSVirtualMachine::handle_state_update(); +} + +void HCSVirtualMachine::compute_system_event_callback(HCS_EVENT* event, void* context) +{ + + const auto type = hcs::parse_event(event); + auto vm = static_cast(context); + + mpl::debug(vm->get_name(), + "compute_system_event_callback() > event: {}, context: {}", + fmt::ptr(event), + fmt::ptr(context)); + + switch (type) + { + case hcs::HcsEventType::SystemExited: + { + mpl::info(vm->get_name(), + "compute_system_event_callback() > {}: SystemExited event received", + vm->get_name()); + vm->state = State::off; + vm->handle_state_update(); + } + break; + case hcs::HcsEventType::Unknown: + default: + mpl::warn(vm->get_name(), + "compute_system_event_callback() > {}: Unidentified event received", + vm->get_name()); + break; + } +} + +std::filesystem::path HCSVirtualMachine::get_guest_state_file_path() const +{ + return std::filesystem::path{description.image.image_path}.replace_extension(".vmgs"); +} +std::filesystem::path HCSVirtualMachine::get_runtime_state_file_path() const +{ + return std::filesystem::path{description.image.image_path}.replace_extension(".vmrs"); +} + +std::filesystem::path HCSVirtualMachine::get_saved_state_file_path() const +{ + return std::filesystem::path{description.image.image_path}.replace_extension( + ".SavedState.vmrs"); +} + +bool HCSVirtualMachine::has_saved_state_file() const +{ + return MP_FILEOPS.exists(get_saved_state_file_path()); +} + +std::filesystem::path HCSVirtualMachine::get_primary_disk_path() const +{ + return description.image.image_path; +} + +void HCSVirtualMachine::grant_access_to_scsi_device(const hcs::HcsScsiDevice& device) const +{ + if (device.type == hcs::HcsScsiDeviceType::VirtualDisk()) + { + std::vector lineage{}; + if (VirtDisk().list_virtual_disk_chain(device.path.get(), lineage)) + { + grant_access_to_paths({lineage.begin(), lineage.end()}); + } + } + else + { + grant_access_to_paths({device.path.get()}); + } +} + +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(get_name(), + "Granting access to path `{}`, exists? {}", + path, + MP_FILEOPS.exists(path)); + if (MP_FILEOPS.is_symlink(path)) + { + paths.push_back(std::filesystem::canonical(path)); + } + + if (const auto r = HCS().grant_vm_access(get_name(), path); !r) + { + mpl::error(get_name(), + "Could not grant access to VM `{}` for the path `{}`, error code: {}", + get_name(), + path, + r); + } + } +} + +void HCSVirtualMachine::set_compute_system_callback_handler() +{ + if (hcs_system) + { + top_catch_all(get_name(), [this] { + if (!HCS().set_compute_system_callback( + hcs_system, + this, + HCSVirtualMachine::compute_system_event_callback)) + { + mpl::warn(get_name(), + "Could not set compute system callback for VM: `{}`!", + get_name()); + } + }); + } +} + +std::vector HCSVirtualMachine::make_endpoint_parameters() const +{ + std::vector params{ + // The primary endpoint (management) + {.network_guid = primary_network_guid, + .endpoint_guid = mac2uuid(description.default_mac_address), + .mac_address = replace_colon_with_dash(description.default_mac_address)}}; + + // Additional endpoints, a.k.a. extra interfaces. + std::ranges::transform(description.extra_interfaces, + std::back_inserter(params), + [](const auto& v) -> hcn::CreateEndpointParameters { + return {.network_guid = + multipass::utils::make_uuid(v.id).toStdString(), + .endpoint_guid = mac2uuid(v.mac_address), + .mac_address = replace_colon_with_dash(v.mac_address)}; + }); + + return params; +}; + +bool HCSVirtualMachine::maybe_create_compute_system() +{ + // Always reset the handle and create a new one. + hcs_system.reset(); + auto attach_callback_handler = + sg::make_scope_guard([this]() noexcept { set_compute_system_callback_handler(); }); + + if (const auto result = HCS().open_compute_system(get_name(), hcs_system)) + { + // Opened existing VM + return false; + } + else if (HCS_E_SYSTEM_NOT_FOUND != static_cast(result.code)) + { + throw OpenComputeSystemException{"Failed with error code: {}", result.code}; + } + + // Create the VM from scratch. + const auto endpoints = make_endpoint_parameters(); + + try_create_endpoints(get_name(), endpoints); + + const hcs::CreateComputeSystemParameters create_compute_system_params{ + .name = description.vm_name, + .memory_size_mb = static_cast(description.mem_size.in_megabytes()), + .processor_count = static_cast(description.num_cores), + .scsi_devices = {{.type = hcs::HcsScsiDeviceType::VirtualDisk(), + .name = "Primary disk", + .path = get_primary_disk_path(), + .read_only = false}, + {.type = hcs::HcsScsiDeviceType::Iso(), + .name = "cloud-init ISO file", + .path = description.cloud_init_iso.toStdString(), + .read_only = true}}, + .network_adapters = + [&] { + const auto view = + endpoints | + std::views::transform([](const auto& endpoint) -> hcs::HcsNetworkAdapter { + return {.endpoint_guid = endpoint.endpoint_guid, + .mac_address = endpoint.mac_address.value()}; + }); + return std::vector(std::ranges::begin(view), std::ranges::end(view)); + }(), + .shares = {}, + .guest_state = {.guest_state_file_path = get_guest_state_file_path(), + .runtime_state_file_path = get_runtime_state_file_path(), + .save_state_file_path = has_saved_state_file() + ? std::optional(get_saved_state_file_path()) + : std::nullopt}}; + + if (const auto create_result = + HCS().create_compute_system(create_compute_system_params, hcs_system); + !create_result) + { + 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_scsi_device(scsi); + } + + // Also grant access to the VM folder itself + grant_access_to_paths({instance_dir.absolutePath().toStdString()}); + return true; +} + +void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) +{ + mpl::debug(get_name(), + "set_state() -> VM `{}` HCS state `{}`", + get_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 = has_saved_state_file() ? State::suspended : State::stopped; + break; + case hcs::ComputeSystemState::unknown: + state = State::unknown; + break; + } + + if (state == prev_state) + return; + + mpl::info(get_name(), + "set_state() > VM {} state changed from {} to {}", + get_name(), + prev_state, + state); +} + +void HCSVirtualMachine::start() +{ + mpl::debug(get_name(), "start() -> Starting VM `{}`, current state {}", get_name(), state); + + // Create the compute system, if not created yet. + if (maybe_create_compute_system()) + mpl::debug(get_name(), + "start() -> VM `{}` was not present, created from scratch", + get_name()); + + const auto prev_state = state; + state = VirtualMachine::State::starting; + handle_state_update(); + // Resume and start are the same thing in Multipass terms + // Try to determine whether we need to resume or start here. + const auto result = [&] { + // Fetch the latest state value. + const auto hcs_state = fetch_state_from_api(); + switch (hcs_state) + { + case hcs::ComputeSystemState::paused: + { + mpl::debug(get_name(), "start() -> VM `{}` is in paused state, resuming", get_name()); + return HCS().resume_compute_system(hcs_system); + } + case hcs::ComputeSystemState::created: + [[fallthrough]]; + default: + { + mpl::debug(get_name(), + "start() -> VM `{}` is in {} state, starting", + get_name(), + state); + return HCS().start_compute_system(hcs_system); + } + } + }(); + + if (!result) + { + state = prev_state; + handle_state_update(); + throw StartComputeSystemException{"Could not start the VM: {}", result}; + } + else if (has_saved_state_file()) + { + mpl::trace(get_name(), "start() -> Saved state file exits, attempting to remove"); + std::error_code ec{}; + if (!MP_FILEOPS.remove(get_saved_state_file_path(), ec)) + { + mpl::warn(get_name(), + "start() -> Could not remove the saved state file, error: {}", + ec); + } + } + + mpl::debug(get_name(), "start() -> result `{}`", get_name(), result); +} +void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) +{ + mpl::debug(get_name(), "shutdown() -> Shutting down, current state {}", get_name(), state); + + try + { + check_state_for_shutdown(shutdown_policy); + } + catch (const VMStateIdempotentException& e) + { + mpl::info(vm_name, "{}", e.what()); + return; + } + + switch (shutdown_policy) + { + case ShutdownPolicy::Powerdown: + mpl::debug(get_name(), "shutdown() -> Requested powerdown, initiating graceful shutdown"); + + // If the guest has integration modules enabled, we can use graceful shutdown. + if (!HCS().shutdown_compute_system(hcs_system)) + { + // Fall back to SSH shutdown. + ssh_exec("sudo shutdown -h now"); + drop_ssh_session(); + } + break; + case ShutdownPolicy::Halt: + case ShutdownPolicy::Poweroff: + mpl::debug(get_name(), + "shutdown() -> Requested halt/poweroff, initiating forceful shutdown"); + // These are non-graceful variants. Just terminate the system immediately. + const auto r = HCS().terminate_compute_system(hcs_system); + mpl::debug(get_name(), "shutdown -> terminate_compute_system result: {}", r.code); + drop_ssh_session(); + break; + } + + // We need to wait here. + auto on_timeout = [] { + throw std::runtime_error("timed out waiting for VM shutdown to complete"); + }; + + multipass::utils::try_action_for(on_timeout, vm_shutdown_timeout, [this]() { + switch (current_state()) + { + case VirtualMachine::State::stopped: + case VirtualMachine::State::off: + return multipass::utils::TimeoutAction::done; + default: + return multipass::utils::TimeoutAction::retry; + } + }); +} + +void HCSVirtualMachine::suspend() +{ + mpl::debug(get_name(), "suspend() -> Suspending VM `{}`, current state {}", get_name(), state); + if (const auto pause_result = HCS().pause_compute_system(hcs_system)) + { + // Pause succeeded. We can suspend to disk now + if (const auto& r = HCS().save_compute_system(hcs_system, get_saved_state_file_path()); r) + { + // Save succeeded. Now, it's safe to terminate the system. + if (const auto& terminate_result = HCS().terminate_compute_system(hcs_system); + !terminate_result) + mpl::warn(get_name(), + "VM suspended successfully but terminate failed, reason: {}", + terminate_result); + } + else + throw SaveComputeSystemException{"Could not save the virtual machine state for VM `{}` " + "to the disk for suspend. Error details: {}", + get_name(), + r}; + } + else + { + throw SaveComputeSystemException{"Could not pause VM for suspend: {}", pause_result}; + } + set_state(fetch_state_from_api()); + handle_state_update(); +} + +HCSVirtualMachine::State HCSVirtualMachine::current_state() +{ + set_state(fetch_state_from_api()); + return state; +} +int HCSVirtualMachine::ssh_port() +{ + return default_ssh_port; +} +std::string HCSVirtualMachine::ssh_hostname(std::chrono::milliseconds /*timeout*/) +{ + return fmt::format("{}.mshome.net", get_name()); +} +std::string HCSVirtualMachine::ssh_username() +{ + return description.ssh_username; +} + +std::optional HCSVirtualMachine::management_ipv4() +{ + const auto& [ipv4, _] = resolve_ip_addresses(ssh_hostname({}).c_str()); + if (ipv4.empty()) + { + mpl::error(get_name(), "management_ipv4() > failed to resolve `{}`", ssh_hostname({})); + return std::nullopt; + } + + const auto result = *ipv4.begin(); + + mpl::trace(get_name(), "management_ipv4() > IP address is `{}`", result); + + // Prefer the first one + return std::make_optional(result); +} + +void HCSVirtualMachine::handle_state_update() +{ + monitor.persist_state_for(get_name(), state); +} + +hcs::ComputeSystemState HCSVirtualMachine::fetch_state_from_api() const +{ + hcs::ComputeSystemState compute_system_state{hcs::ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(hcs_system, compute_system_state); + return compute_system_state; +} + +void HCSVirtualMachine::update_cpus(int num_cores) +{ + mpl::debug(get_name(), + "update_cpus() -> called for VM `{}`, num_cores `{}`", + get_name(), + num_cores); + description.num_cores = num_cores; +} + +void HCSVirtualMachine::resize_memory(const MemorySize& new_size) +{ + mpl::debug(get_name(), + "resize_memory() -> called for VM `{}`, new_size `{}` MiB", + get_name(), + new_size.in_megabytes()); + description.mem_size = new_size; +} + +void HCSVirtualMachine::resize_disk(const MemorySize& new_size) +{ + mpl::debug(get_name(), + "resize_disk() -> called for VM `{}`, new_size `{}` MiB", + get_name(), + new_size.in_megabytes()); + + if (const auto result = + VirtDisk().resize_virtual_disk(description.image.image_path, new_size.in_bytes()); + !result) + { + throw ResizeDiskException{"Disk resize failed, details: {}", result}; + } + description.disk_space = new_size; +} + +void HCSVirtualMachine::add_network_interface(int index, + const std::string& default_mac_addr, + const NetworkInterface& extra_interface) +{ + mpl::debug(get_name(), + "add_network_interface() -> called for VM `{}`, index: {}, default_mac: {}, " + "extra_interface: (mac: {}, " + "mac_address: {}, id: {})", + get_name(), + index, + default_mac_addr, + extra_interface.mac_address, + extra_interface.auto_mode, + extra_interface.id); + add_extra_interface_to_instance_cloud_init(default_mac_addr, extra_interface); + if (!(state == VirtualMachine::State::stopped)) + { + // No need to do it for stopped machines + mpl::info(get_name(), + "add_network_interface() -> Skipping hot-plug, VM is in a stopped state."); + return; + } +} +std::unique_ptr +HCSVirtualMachine::make_native_mount_handler(const std::string& target, const VMMount& mount) +{ + mpl::debug(get_name(), + "make_native_mount_handler() -> called for VM `{}`, target: {}", + get_name(), + target); + + 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) +{ + throw NotImplementedOnThisBackendException{"snapshot"}; +} + +std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const QString& filename) +{ + throw NotImplementedOnThisBackendException{"snapshot"}; +} + +} // 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 00000000000..8d8ecec5e45 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +#include + +struct HCS_EVENT; + +namespace multipass +{ +class VMStatusMonitor; +} + +namespace multipass::hyperv +{ + +namespace hcs +{ +struct HcsScsiDevice; +} + +/** + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. + */ +struct HCSVirtualMachine : public BaseVirtualMachine +{ + HCSVirtualMachine(const std::string& network_guid, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& instance_dir); + + HCSVirtualMachine(const std::string& source_vm_name, + const multipass::VMSpecs& src_vm_specs, + const VirtualMachineDescription& desc, + VMStatusMonitor& monitor, + const SSHKeyProvider& key_provider, + const Path& dest_instance_dir); + + void start() override; + void shutdown(ShutdownPolicy shutdown_policy) override; + void suspend() override; + [[nodiscard]] State current_state() override; + int ssh_port() override; + [[nodiscard]] std::string ssh_hostname(std::chrono::milliseconds timeout) override; + [[nodiscard]] std::string ssh_username() override; + [[nodiscard]] std::optional management_ipv4() override; + + void handle_state_update() 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; + [[nodiscard]] std::unique_ptr + make_native_mount_handler(const std::string& target, const VMMount& mount) override; + +protected: + [[nodiscard]] std::shared_ptr make_specific_snapshot( + const QString& filename) override; + [[nodiscard]] 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: + VirtualMachineDescription description{}; + const std::string primary_network_guid{}; + VMStatusMonitor& monitor; + + hcs::HcsSystemHandle hcs_system{nullptr}; + + [[nodiscard]] hcs::ComputeSystemState fetch_state_from_api() const; + 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 + */ + [[nodiscard]] bool maybe_create_compute_system() noexcept(false); + + /** + * Retrieve path to the primary disk symbolic link + */ + [[nodiscard]] std::filesystem::path get_primary_disk_path() const noexcept(false); + + [[nodiscard]] std::filesystem::path get_guest_state_file_path() const; + [[nodiscard]] std::filesystem::path get_runtime_state_file_path() const; + [[nodiscard]] std::filesystem::path get_saved_state_file_path() const; + [[nodiscard]] bool has_saved_state_file() const; + + void grant_access_to_scsi_device(const hcs::HcsScsiDevice& device) const; + void grant_access_to_paths(std::list paths) const; + + static void compute_system_event_callback(HCS_EVENT* event, void* context); + + void set_compute_system_callback_handler(); + + std::vector make_endpoint_parameters() const; +}; +} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h new file mode 100644 index 00000000000..5db1cfd7d8a --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass::hyperv +{ + +struct InvalidAPIPointerException : FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct CreateComputeSystemException : FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct OpenComputeSystemException : 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; +}; + +struct ResizeDiskException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct CreateBridgeException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct WindowsFeatureNotEnabledException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct SaveComputeSystemException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +} // namespace multipass::hyperv 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 00000000000..bb175592a25 --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp @@ -0,0 +1,355 @@ +/* + * 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 // for the std::filesystem::path formatter + +namespace +{ +void update_adapter_authorizations(std::vector& adapters, + const std::vector& switches) +{ + for (auto& adapter : adapters) + adapter.needs_authorization = + std::none_of(switches.cbegin(), switches.cend(), [&adapter](const auto& switch_) { + return std::find(switch_.links.cbegin(), switch_.links.cend(), adapter.id) != + switch_.links.cend(); + }); +} + +} // namespace + +namespace multipass::hyperv +{ + +using hcn::HCN; +using hcs::HCS; +using virtdisk::VirtDisk; + +constexpr auto log_category = "HyperV-Virtual-Machine-Factory"; +constexpr auto default_hyperv_switch_guid = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444"; +constexpr auto extra_interface_vswitch_name_fmtstr = "Multipass vSwitch ({})"; +/** + * Regex pattern to extract the origin network name and GUID from an extra interface + * name. + */ +constexpr auto extra_interface_vswitch_name_regex = R"(Multipass vSwitch \((.*)\))"; + +HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir) + : BaseVirtualMachineFactory( + MP_UTILS.derive_instances_dir(data_dir, + HCSVirtualMachineFactory::get_backend_directory_name(), + instances_subdir)) +{ +} + +VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine( + const VirtualMachineDescription& desc, + const SSHKeyProvider& key_provider, + VMStatusMonitor& monitor) +{ + return std::make_unique(default_hyperv_switch_guid, + desc, + monitor, + key_provider, + get_instance_directory(desc.vm_name)); +} + +void HCSVirtualMachineFactory::remove_resources_for_impl(const std::string& name) +{ + mpl::debug(log_category, "remove_resources_for_impl() -> VM: {}", name); + hcs::HcsSystemHandle handle{nullptr}; + if (HCS().open_compute_system(name, handle)) + { + // Grab compute system GUID before terminating it so we can use it later on for endpoint + // cleanup. + std::string vm_guid{}; + if (!HCS().get_compute_system_guid(handle, vm_guid) || vm_guid.empty()) + { + mpl::warn(log_category, + "Could not retrieve VM guid for `{}`, skipping endpoint cleanup.", + name); + return; + } + // 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 terminate_result = HCS().terminate_compute_system(handle); + if (terminate_result) + { + mpl::warn(log_category, + "remove_resources_for_impl() -> Host compute system {} was still alive.", + name); + } + + std::vector attached_endpoints{}; + const auto& enumerate_result = + HCN().enumerate_attached_endpoints(vm_guid, attached_endpoints); + for (const auto& elem : attached_endpoints) + { + const auto remove_result = HCN().delete_endpoint(elem); + + mpl::log(remove_result ? mpl::Level::trace : mpl::Level::warning, + log_category, + "remove_resources_for_impl() -> Remove attached endpoint {}: {}", + elem, + remove_result.code); + } + } + else + { + mpl::info(log_category, + "remove_resources_for_impl() -> Host compute system `{}` already terminated.", + name); + } +} + +VMImage HCSVirtualMachineFactory::prepare_source_image(const VMImage& source_image) +{ + const auto& source_file = source_image.image_path; + if (!MP_FILEOPS.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", + MP_PLATFORM.path_to_qstr(source_file), + MP_PLATFORM.path_to_qstr(target_file)}; + + 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(image_resize_timeout)) + { + throw ImageConversionException{"Conversion of image {} to VHDX timed out", source_file}; + } + + if (qemu_img_process.exitStatus() != QProcess::NormalExit || 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 = target_file.generic_wstring(); + return result; +} + +void HCSVirtualMachineFactory::prepare_instance_image(const VMImage& instance_image, + const VirtualMachineDescription& desc) +{ + // Resize the instance image to the desired size + const auto resize_result = + VirtDisk().resize_virtual_disk(instance_image.image_path, desc.disk_space.in_bytes()); + if (!resize_result) + { + throw ImageResizeException{"Failed to resize VHDX file `{}`, virtdisk API error code `{}`", + instance_image.image_path, + resize_result}; + } +} + +std::string HCSVirtualMachineFactory::create_bridge_with(const NetworkInterfaceInfo& intf) +{ + const auto vswitch_name = fmt::format(extra_interface_vswitch_name_fmtstr, intf.id); + const auto params = [&intf, &vswitch_name] { + hcn::CreateNetworkParameters network_params{}; + network_params.name = vswitch_name; + network_params.type = hcn::HcnNetworkType::Transparent(); + network_params.guid = 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; + }(); + + const auto create_network_result = HCN().create_network(params); + + if (create_network_result || + static_cast(create_network_result.code) == HCN_E_NETWORK_ALREADY_EXISTS) + { + return params.name; + } + + throw CreateBridgeException{"Could not create vSwitch `{}`, status: {}", + params.name, + create_network_result}; +} + +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!"}; + } + + const auto it = std::ranges::find_if( + fs::directory_iterator{src_vm_instance_dir}, + [](const fs::directory_entry& e) { return e.path().extension() == ".vhdx"; }); + + if (it == std::default_sentinel) + { + throw std::runtime_error{"Could not locate source VM's vhdx file!"}; + } + + const auto src_vm_vhdx = it->path(); + + // Copy the VHDX file. + const hyperv::virtdisk::CreateVirtualDiskParameters clone_vhdx_params{ + .size_in_bytes = 0, // 512 MiB + .path = desc.image.image_path, + .predecessor = virtdisk::SourcePathParameters{src_vm_vhdx}}; + + const auto create_vd_result = VirtDisk().create_virtual_disk(clone_vhdx_params); + + if (!create_vd_result) + { + 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::get_hyperv_vswitches() +{ + std::vector result; + std::vector hyperv_network_guids; + std::vector hyperv_network_infos; + + if (const auto enumerate_result = HCN().enumerate_networks(hyperv_network_guids)) + { + for (const auto& network_guid : hyperv_network_guids) + { + if (const auto result = + HCN().query_network(network_guid, hyperv_network_infos.emplace_back()); + !result) + { + mpl::warn(log_category, + "Could not retrieve network information for {}, result: {}", + network_guid, + result); + hyperv_network_infos.pop_back(); + } + } + + for (const auto& network_info : hyperv_network_infos) + { + result.emplace_back(NetworkInterfaceInfo{ + .id = network_info.name, + .type = MP_PLATFORM.bridge_nomenclature(), + .description = fmt::format("Hyper-V vSwitch({})", network_info.type), + .links = network_info.network_adapter_name.has_value() + ? std::vector{network_info.network_adapter_name.value()} + : std::vector{}, + .needs_authorization = false}); + } + } + else + { + mpl::warn(log_category, "Network enumeration failed, result: {}", enumerate_result); + } + + return result; +} + +std::vector HCSVirtualMachineFactory::networks() const +{ + auto adapters = get_adapters(); + auto switches = get_hyperv_vswitches(); + update_adapter_authorizations(adapters, switches); + + if (adapters.size() > switches.size()) + std::swap(adapters, switches); // we want to move the smallest one + + switches.reserve(adapters.size() + switches.size()); // avoid growing more times than needed + std::move(adapters.begin(), adapters.end(), std::back_inserter(switches)); + return switches; +} + +void HCSVirtualMachineFactory::hypervisor_health_check() +{ + if (auto state = get_windows_feature_state(L"VirtualMachinePlatform")) + { + if (state != WindowsFeatureState::Enabled) + { + throw WindowsFeatureNotEnabledException{ + "Hyper-V HCS backend requires `Virtual Machine Platform` feature to be enabled. " + "Current state is : {0}", + state == WindowsFeatureState::Absent ? "Absent" : "Disabled"}; + } + } +} + +} // 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 00000000000..1de91ef12ed --- /dev/null +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_factory.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace multipass::hyperv +{ + +/** + * Native Windows virtual machine implementation using HCS, HCN & virtdisk API's. + */ +struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory +{ + + HCSVirtualMachineFactory(const Path& data_dir); + + [[nodiscard]] VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, + const SSHKeyProvider& key_provider, + VMStatusMonitor& monitor) override; + + [[nodiscard]] 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; + + [[nodiscard]] QString get_backend_version_string() const override + { + return "hyperv_api"; + }; + + [[nodiscard]] std::vector networks() const override; + +protected: + [[nodiscard]] std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; + void remove_resources_for_impl(const std::string& name) override; + +private: + [[nodiscard]] 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; + /** + * Retrieve a list of available network adapters. + */ + [[nodiscard]] static std::vector get_adapters(); + [[nodiscard]] static std::vector get_hyperv_vswitches(); +}; +} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hyperv_api_operation_result.h b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h new file mode 100644 index 00000000000..d1b20a006ae --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_operation_result.h @@ -0,0 +1,152 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +#include +#include + +namespace multipass::hyperv +{ + +/** + * A simple HRESULT wrapper which is boolable for + * convenience. + */ +struct ResultCode +{ + using unsigned_hresult_t = std::make_unsigned_t; + + ResultCode(HRESULT r) noexcept : result(r) + { + } + + ResultCode& operator=(HRESULT r) noexcept + { + result = r; + return *this; + } + + bool success() const noexcept + { + return static_cast(*this) == S_OK; + } + + [[nodiscard]] explicit operator HRESULT() const noexcept + { + return result; + } + + [[nodiscard]] explicit operator unsigned_hresult_t() const noexcept + { + return static_cast(result); + } + + [[nodiscard]] explicit operator std::error_code() const noexcept + { + return std::error_code{result, std::system_category()}; + } + +private: + HRESULT result{}; +}; + +/** + * An object that describes the result of an HCN operation + * performed through HCNWrapper. + */ +struct OperationResult +{ + + /** + * Status code of the operation. Equal to + * S_OK on success. + */ + ResultCode code; + + /** + * A message that describes the result of the operation. + * It might contain an error message describing the error + * when the operation fails, or details regarding the status + * of a successful operation. + */ + std::wstring status_msg; + + [[nodiscard]] explicit operator bool() const noexcept + { + return static_cast(code) == S_OK; + } + + [[nodiscard]] operator std::error_code() const noexcept + { + return static_cast(code); + } +}; +} // namespace multipass::hyperv + +/** + * Formatter type specialization for ResultCode + */ +template +struct fmt::formatter + : formatter, Char> +{ + + std::string hint(const multipass::hyperv::ResultCode& rc) const + { + switch (static_cast>(rc)) + { + // HCN: There are no more endpoints available from the endpoint mapper. + case 0x800706d9: + // HCS: The operation could not be started because a required feature is not installed + case 0x80370114: + return {"(Hint: Did you enable the `Virtual Machine Platform` feature?)"}; + } + return ""; + } + + template + auto format(const multipass::hyperv::ResultCode& rc, FormatContext& ctx) const + { + const std::error_code ec{static_cast(rc), std::system_category()}; + const auto hint_r = hint(rc); + return format_to(ctx.out(), + "{:#x}: {}{}", + static_cast>(rc), + ec.message(), + hint_r); + } +}; + +/** + * Formatter type specialization for ResultCode + */ +template +struct fmt::formatter + : formatter, Char> +{ + + template + auto format(const multipass::hyperv::OperationResult& opr, FormatContext& ctx) const + { + 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 00000000000..99cdd2d398b --- /dev/null +++ b/src/platform/backends/hyperv_api/hyperv_api_string_conversion.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace multipass::hyperv +{ + +inline constexpr auto to_wstring(auto&& value) +{ + // TODO: This has been deprecated. Replace with Boost.nowide or equivalent. + return std::wstring_convert>().from_bytes(value.data(), + value.data() + + value.size()); +} + +namespace detail +{ + +template +struct string_literal +{ + std::array storage; + + consteval string_literal(const char (&str)[N]) + { + if constexpr (std::is_same_v) + std::ranges::copy(str, storage.begin()); + else + { + std::ranges::transform(str, storage.begin(), [](char c) { + auto uc = static_cast(c); + if (uc > 127) + throw "non-ASCII character in universal_literal"; + return static_cast(uc); + }); + } + } + + constexpr operator fmt::basic_string_view() const + { + return fmt::basic_string_view{storage.data(), N - 1}; + } + + constexpr operator std::basic_string_view() const + { + return std::basic_string_view{storage.data(), N - 1}; + } +}; + +template +class formattable_string_literal : public string_literal +{ + template + static constexpr decltype(auto) adapt(T&& arg); + +public: + using string_literal::string_literal; + + template + constexpr auto format_to(FormatContext& ctx, Args&&... args) const + { + return fmt::format_to(ctx.out(), fmt::runtime(*this), adapt(std::forward(args))...); + } + + template + constexpr auto format(Args&&... args) const + { + return fmt::format(fmt::runtime(*this), adapt(std::forward(args))...); + } +}; + +template +concept narrow_string_like = std::is_convertible_v; + +template +template +inline constexpr decltype(auto) formattable_string_literal::adapt(T&& arg) +{ + using D = std::decay_t; + + if constexpr (std::is_same_v && narrow_string_like) + { + return to_wstring(std::string_view{std::forward(arg)}); + } + else + return std::forward(arg); +} +} // namespace detail + +template +consteval auto string_literal(const char (&str)[N]) -> detail::formattable_string_literal +{ + return detail::formattable_string_literal{str}; +} + +} // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.cpp new file mode 100644 index 00000000000..6ed6cd8d63d --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.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 + +namespace multipass::hyperv::virtdisk +{ + +VirtDiskAPI::VirtDiskAPI(const Singleton::PrivatePass& pass) noexcept + : Singleton(pass) +{ +} + +DWORD VirtDiskAPI::CreateVirtualDisk(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) const +{ + return ::CreateVirtualDisk(VirtualStorageType, + Path, + VirtualDiskAccessMask, + SecurityDescriptor, + Flags, + ProviderSpecificFlags, + Parameters, + Overlapped, + Handle); +} +DWORD VirtDiskAPI::OpenVirtualDisk(PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) const +{ + return ::OpenVirtualDisk(VirtualStorageType, + Path, + VirtualDiskAccessMask, + Flags, + Parameters, + Handle); +} +DWORD VirtDiskAPI::ResizeVirtualDisk(HANDLE VirtualDiskHandle, + RESIZE_VIRTUAL_DISK_FLAG Flags, + PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) const +{ + return ::ResizeVirtualDisk(VirtualDiskHandle, Flags, Parameters, Overlapped); +} +DWORD VirtDiskAPI::MergeVirtualDisk(HANDLE VirtualDiskHandle, + MERGE_VIRTUAL_DISK_FLAG Flags, + PMERGE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) const +{ + return ::MergeVirtualDisk(VirtualDiskHandle, Flags, Parameters, Overlapped); +} +DWORD VirtDiskAPI::GetVirtualDiskInformation(HANDLE VirtualDiskHandle, + PULONG VirtualDiskInfoSize, + PGET_VIRTUAL_DISK_INFO VirtualDiskInfo, + PULONG SizeUsed) const +{ + return ::GetVirtualDiskInformation(VirtualDiskHandle, + VirtualDiskInfoSize, + VirtualDiskInfo, + SizeUsed); +} +DWORD VirtDiskAPI::SetVirtualDiskInformation(HANDLE VirtualDiskHandle, + PSET_VIRTUAL_DISK_INFO VirtualDiskInfo) const +{ + return ::SetVirtualDiskInformation(VirtualDiskHandle, VirtualDiskInfo); +} +BOOL VirtDiskAPI::CloseHandle(HANDLE hObject) const +{ + return ::CloseHandle(hObject); +} + +} // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.h new file mode 100644 index 00000000000..42137267e28 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_api.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace multipass::hyperv::virtdisk +{ + +/** + * API function table for the virtdisk API + * @ref https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ + */ +struct VirtDiskAPI : public Singleton +{ + VirtDiskAPI(const Singleton::PrivatePass&) noexcept; + [[nodiscard]] virtual DWORD CreateVirtualDisk(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) const; + [[nodiscard]] virtual DWORD OpenVirtualDisk(PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle) const; + [[nodiscard]] virtual DWORD ResizeVirtualDisk(HANDLE VirtualDiskHandle, + RESIZE_VIRTUAL_DISK_FLAG Flags, + PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) const; + [[nodiscard]] virtual DWORD MergeVirtualDisk(HANDLE VirtualDiskHandle, + MERGE_VIRTUAL_DISK_FLAG Flags, + PMERGE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) const; + [[nodiscard]] virtual DWORD GetVirtualDiskInformation(HANDLE VirtualDiskHandle, + PULONG VirtualDiskInfoSize, + PGET_VIRTUAL_DISK_INFO VirtualDiskInfo, + PULONG SizeUsed) const; + [[nodiscard]] virtual DWORD SetVirtualDiskInformation( + HANDLE VirtualDiskHandle, + PSET_VIRTUAL_DISK_INFO VirtualDiskInfo) const; + [[nodiscard]] virtual BOOL CloseHandle(HANDLE hObject) const; +}; + +} // namespace multipass::hyperv::virtdisk 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 new file mode 100644 index 00000000000..44897b6430e --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_create_virtual_disk_params.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +#include + +namespace multipass::hyperv::virtdisk +{ + +/** + * Source disk to copy the data from. This is used for cloning an existing disk into a new disk. + */ +struct SourcePathParameters +{ + std::filesystem::path path; +}; + +/** + * Parent disk information. This is used for creating a virtual disk chain to layer disks. + */ +struct ParentPathParameters +{ + std::filesystem::path path; +}; + +struct VirtualDiskPredecessorInfo +{ + VirtualDiskPredecessorInfo() = default; + + VirtualDiskPredecessorInfo(const SourcePathParameters& param) : predecessor{param} + { + if (param.path.empty()) + { + throw std::invalid_argument{"Source disk path cannot be empty."}; + } + } + + VirtualDiskPredecessorInfo(const ParentPathParameters& param) : predecessor{param} + { + if (param.path.empty()) + { + throw std::invalid_argument{"Parent disk path cannot be empty."}; + } + } + + const auto& get() const + { + return predecessor; + } + +private: + std::variant predecessor{ + std::monostate{}}; +}; + +/** + * Parameters for creating a new virtual disk drive. + */ +struct CreateVirtualDiskParameters +{ + std::uint64_t size_in_bytes{}; + std::filesystem::path path{}; + /** + * Monostate: A new disk. + * + * SourcePathParameters: A new disk, data and properties cloned from the disk specified by + * SourcePathParameters. + * + * ParentPathParameters: A new disk, layered onto an existing disk. The + * existing disk can be a VHDX or AVHDX. + */ + VirtualDiskPredecessorInfo predecessor{}; +}; + +} // namespace multipass::hyperv::virtdisk + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::virtdisk::CreateVirtualDiskParameters& params, + FormatContext& ctx) const + { + return fmt::format_to(ctx.out(), + "Size (in bytes): ({}) | Path: ({}) ", + params.size_in_bytes, + params.path.string()); + } +}; diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h new file mode 100644 index 00000000000..55b22943b20 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_disk_info.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +#include +#include + +namespace multipass::hyperv::virtdisk +{ + +struct VirtualDiskInfo +{ + struct size_info + { + std::uint64_t virtual_{}; + std::uint64_t physical{}; + std::uint64_t block{}; + std::uint64_t sector{}; + }; + std::optional size{}; + std::optional smallest_safe_virtual_size{}; + std::optional virtual_storage_type{}; +}; + +} // namespace multipass::hyperv::virtdisk + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::virtdisk::VirtualDiskInfo::size_info& params, + FormatContext& ctx) const + { + return fmt::format_to(ctx.out(), + "Virtual: ({}) | Physical: ({}) | Block: ({}) | Sector: ({})", + params.virtual_, + params.physical, + params.block, + params.sector); + } +}; + +/** + * Formatter type specialization for CreateComputeSystemParameters + */ +template +struct fmt::formatter + : formatter, Char> +{ + template + auto format(const multipass::hyperv::virtdisk::VirtualDiskInfo& params, + FormatContext& ctx) const + { + return fmt::format_to(ctx.out(), + "Storage type: {} | Size: {} | Smallest safe size: {}", + params.virtual_storage_type, + params.size, + params.smallest_safe_virtual_size); + } +}; diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.cpp b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.cpp new file mode 100644 index 00000000000..2b3aa7d20be --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.cpp @@ -0,0 +1,632 @@ +/* + * 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 + +namespace multipass::hyperv::virtdisk +{ + +using ztd::out_ptr::out_ptr; +namespace mpl = logging; +using lvl = mpl::Level; + +namespace +{ + +constexpr auto log_category = "HyperV-VirtDisk-Wrapper"; + +// --------------------------------------------------------- + +inline const VirtDiskAPI& API() +{ + return VirtDiskAPI::instance(); +} + +// --------------------------------------------------------- + +// helper type for the visitor #4 +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; + +// --------------------------------------------------------- + +auto normalize_path(std::filesystem::path p) +{ + p.make_preferred(); + return p; +} + +// --------------------------------------------------------- + +struct HandleCloser +{ + void operator()(HANDLE h) const noexcept + { + (void)API().CloseHandle(h); + } +}; + +using UniqueHandle = std::unique_ptr, HandleCloser>; + +// --------------------------------------------------------- + +struct VirtDiskCreateError : FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +struct VirtDiskPredecessorError : FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + +// --------------------------------------------------------- + +UniqueHandle open_virtual_disk( + 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(log_category, "open_virtual_disk(...) > vhdx_path: {}", vhdx_path); + // + // Specify UNKNOWN for both device and vendor so the system will use the + // file extension to determine the correct VHD format. + // + VIRTUAL_STORAGE_TYPE type{}; + type.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN; + type.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN; + + UniqueHandle handle{nullptr}; + const auto path_w = vhdx_path.generic_wstring(); + + const ResultCode result = API().OpenVirtualDisk( + // [in] PVIRTUAL_STORAGE_TYPE VirtualStorageType + &type, + // [in] PCWSTR Path + path_w.c_str(), + // [in] VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask + access_mask, + // [in] OPEN_VIRTUAL_DISK_FLAG Flags + flags, + // [in, optional] POPEN_VIRTUAL_DISK_PARAMETERS Parameters + params, + // [out] PHANDLE Handle + out_ptr(handle)); + + if (!result.success()) + { + mpl::error(log_category, + "open_virtual_disk(...) > OpenVirtualDisk failed with: {}", + static_cast(result)); + return UniqueHandle{nullptr}; + } + + return handle; +} + +// --------------------------------------------------------- + +void fill_predecessor_info(const VirtDiskWrapper& wrapper, + 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 VirtDiskPredecessorError{"Predecessor VHDX file `{}` does not exist!", pp}; + } + + target_path = predecessor_path.c_str(); + VirtualDiskInfo predecessor_disk_info{}; + const auto result = wrapper.get_virtual_disk_info(predecessor_path, predecessor_disk_info); + mpl::debug(log_category, + "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 VirtDiskPredecessorError{"Unable to determine predecessor disk's (`{}`) type!", + pp}; + } + else + { + throw VirtDiskPredecessorError{"Unsupported predecessor disk type"}; + } + } + else + { + throw VirtDiskPredecessorError{ + "Failed to retrieve the predecessor disk type for `{}`, error code: {}", + pp, + result}; + } +} + +} // namespace + +// --------------------------------------------------------- + +VirtDiskWrapper::VirtDiskWrapper(const Singleton::PrivatePass& pass) noexcept + : Singleton::Singleton(pass) +{ +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::create_virtual_disk( + const CreateVirtualDiskParameters& params) const +{ + mpl::debug(log_category, "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 + // + VIRTUAL_STORAGE_TYPE type{}; + + // + // 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_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; + 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{}; + + 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_predecessor_info(*this, + predecessor_path_normalized, + parameters.Version2.SourcePath, + parameters.Version2.SourceVirtualStorageType); + flags |= CREATE_VIRTUAL_DISK_FLAG_PREVENT_WRITES_TO_SOURCE_DISK; + mpl::debug(log_category, + "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_predecessor_info(*this, + 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.get()); + + UniqueHandle result_handle{nullptr}; + + const auto result = + API().CreateVirtualDisk(&type, + // [in] PCWSTR Path + 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, + flags, + // [in] ULONG ProviderSpecificFlags, + 0, + // [in] PCREATE_VIRTUAL_DISK_PARAMETERS Parameters, + ¶meters, + // [in, optional] LPOVERLAPPED Overlapped + nullptr, + // [out] PHANDLE Handle + out_ptr(result_handle)); + + if (result == ERROR_SUCCESS) + { + return OperationResult{NOERROR, L""}; + } + + mpl::error(log_category, + "create_virtual_disk(...) > CreateVirtualDisk failed with {}!", + result); + return OperationResult{E_FAIL, fmt::format(L"CreateVirtualDisk failed with {}!", result)}; +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::resize_virtual_disk(const std::filesystem::path& vhdx_path, + std::uint64_t new_size_bytes) const +{ + mpl::debug(log_category, + "resize_virtual_disk(...) > vhdx_path: {}, new_size_bytes: {}", + vhdx_path, + new_size_bytes); + const auto disk_handle = open_virtual_disk(vhdx_path); + + if (nullptr == disk_handle) + { + return OperationResult{E_FAIL, L"open_virtual_disk failed!"}; + } + + RESIZE_VIRTUAL_DISK_PARAMETERS params{}; + params.Version = RESIZE_VIRTUAL_DISK_VERSION_1; + params.Version1 = {}; + params.Version1.NewSize = new_size_bytes; + + const auto resize_result = API().ResizeVirtualDisk( + // [in] HANDLE VirtualDiskHandle + disk_handle.get(), + // [in] RESIZE_VIRTUAL_DISK_FLAG Flags + RESIZE_VIRTUAL_DISK_FLAG_NONE, + // [in] PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters + ¶ms, + // [in, optional] LPOVERLAPPED Overlapped + nullptr); + + if (ERROR_SUCCESS == resize_result) + { + return OperationResult{NOERROR, L""}; + } + + mpl::error(log_category, + "resize_virtual_disk(...) > ResizeVirtualDisk failed with {}!", + resize_result); + + return OperationResult{E_FAIL, + fmt::format(L"ResizeVirtualDisk failed with {}!", resize_result)}; +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::merge_virtual_disk_into_parent( + const std::filesystem::path& child) const +{ + // https://github.com/microsoftarchive/msdn-code-gallery-microsoft/blob/21cb9b6bc0da3b234c5854ecac449cb3bd261f29/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(log_category, "merge_virtual_disk_into_parent(...) > child: {}", child); + + 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(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(log_category, + "merge_virtual_disk_into_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(log_category, + "reparent_virtual_disk(...) > child: {}, new parent: {}", + child, + parent); + + 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(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(log_category, + "reparent_virtual_disk(...) > SetVirtualDiskInformation failed with {}!", + r); + return OperationResult{E_FAIL, fmt::format(L"reparent_virtual_disk failed with {}!", r)}; + } +} + +// --------------------------------------------------------- + +VirtualDiskInfo::size_info read_virtual_disk_info_size_info(const GET_VIRTUAL_DISK_INFO& disk_info) +{ + return { + .virtual_ = disk_info.Size.VirtualSize, + .physical = disk_info.Size.PhysicalSize, + .block = disk_info.Size.BlockSize, + .sector = disk_info.Size.SectorSize, + }; +} + +// --------------------------------------------------------- + +std::optional read_virtual_disk_info_type(const GET_VIRTUAL_DISK_INFO& disk_info) +{ + switch (disk_info.VirtualStorageType.DeviceId) + { + case VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN: + return "unknown"; + case VIRTUAL_STORAGE_TYPE_DEVICE_ISO: + return "iso"; + case VIRTUAL_STORAGE_TYPE_DEVICE_VHD: + return "vhd"; + case VIRTUAL_STORAGE_TYPE_DEVICE_VHDX: + return "vhdx"; + break; + case VIRTUAL_STORAGE_TYPE_DEVICE_VHDSET: + return "vhdset"; + default: + return "unknown"; + } + return std::nullopt; +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::get_virtual_disk_info(const std::filesystem::path& vhdx_path, + VirtualDiskInfo& vdinfo) const +{ + mpl::debug(log_category, "get_virtual_disk_info(...) > vhdx_path: {}", vhdx_path); + // + // https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Hyper-V/Storage/cpp/GetVirtualDiskInformation.cpp + // + + const auto disk_handle = open_virtual_disk(vhdx_path); + + if (nullptr == disk_handle) + { + return OperationResult{E_FAIL, L"open_virtual_disk failed!"}; + } + + constexpr GET_VIRTUAL_DISK_INFO_VERSION what_to_get[] = { + GET_VIRTUAL_DISK_INFO_SIZE, + GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE}; + + for (const auto version : what_to_get) + { + GET_VIRTUAL_DISK_INFO disk_info{}; + disk_info.Version = version; + + ULONG sz = sizeof(disk_info); + + const auto result = + API().GetVirtualDiskInformation(disk_handle.get(), &sz, &disk_info, nullptr); + + if (ERROR_SUCCESS == result) + { + switch (disk_info.Version) + { + case GET_VIRTUAL_DISK_INFO_SIZE: + vdinfo.size = read_virtual_disk_info_size_info(disk_info); + break; + case GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE: + vdinfo.virtual_storage_type = read_virtual_disk_info_type(disk_info); + break; + case GET_VIRTUAL_DISK_INFO_SMALLEST_SAFE_VIRTUAL_SIZE: + vdinfo.smallest_safe_virtual_size = disk_info.SmallestSafeVirtualSize; + break; + default: + assert(0 && "unhandled version"); + break; + } + } + else + { + mpl::warn(log_category, + "get_virtual_disk_info(...) > failed to get {}", + fmt::underlying(version)); + } + } + + return {NOERROR, L""}; +} + +// --------------------------------------------------------- + +OperationResult VirtDiskWrapper::list_virtual_disk_chain(const std::filesystem::path& vhdx_path, + std::vector& chain, + std::optional max_depth) const +{ + + mpl::debug(log_category, "list_virtual_disk_chain(...) > vhdx_path: {}", vhdx_path); + // https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Hyper-V/Storage/cpp/GetVirtualDiskInformation.cpp#L285 + + // Check if given vhdx is a differencing disk. + std::filesystem::path current = vhdx_path; + + auto allocate_disk_info = [](const std::size_t sz = sizeof(GET_VIRTUAL_DISK_INFO)) { + return std::unique_ptr( + static_cast(std::malloc(sz)), + std::free); + }; + + do + { + // Heap-alloc since we're going to re-allocate it for the trailing + // variable length array. + auto disk_info = allocate_disk_info(); + disk_info->Version = GET_VIRTUAL_DISK_INFO_PARENT_LOCATION; + + const auto disk_handle = open_virtual_disk(current); + + if (nullptr == disk_handle) + return OperationResult{E_FAIL, L"open_virtual_disk failed!"}; + + chain.push_back(current); + + ULONG sz = sizeof(GET_VIRTUAL_DISK_INFO); + + // Here we are calling the GetVirtualDiskInformation function to obtain the parent disk + // path, if any. The API stores the parent's path into a variable array field, which needs + // to be allocated by us. Hence, the API first returns ERROR_INSUFFICIENT_BUFFER to tell us + // how much extra space is needed for storing the parent's path. + // If the disk does not have a parent, the API would return ERROR_VHD_INVALID_TYPE instead. + if (const auto r = + GetVirtualDiskInformation(disk_handle.get(), &sz, disk_info.get(), nullptr); + r == ERROR_INSUFFICIENT_BUFFER) + { + // Reallocate the disk_info struct with the correct size, and also re-set + // the version field as it's not an in-place re-allocation. + disk_info = allocate_disk_info(sz); + disk_info->Version = GET_VIRTUAL_DISK_INFO_PARENT_LOCATION; + } + else if (r == ERROR_VHD_INVALID_TYPE) + { + // End of the chain, or not a chain at all. + // Either way, end the loop. + break; + } + else + return OperationResult{E_FAIL, L"GetVirtualDiskInformation failed!"}; + + // This is the real call to obtain the parent path. + const auto r = GetVirtualDiskInformation(disk_handle.get(), &sz, disk_info.get(), nullptr); + + if (r == ERROR_SUCCESS) + { + // The ParentLocationBuffer field is multi-purposed. It might contain a single string, + // or multiple strings, each denoting a possible path for the VHDX file. + if (disk_info->ParentLocation.ParentResolved) + { + // Single string. + const auto parent_buffer_size = + sz - FIELD_OFFSET(GET_VIRTUAL_DISK_INFO, ParentLocation.ParentLocationBuffer); + std::size_t parent_path_size = {0}; + if (FAILED(StringCbLengthW(disk_info->ParentLocation.ParentLocationBuffer, + parent_buffer_size, + &parent_path_size))) + { + return OperationResult{E_FAIL, L"StringCbLengthW failed!"}; + } + current = std::wstring{disk_info->ParentLocation.ParentLocationBuffer, + parent_path_size / sizeof(wchar_t)}; + continue; + } + else + // If ParentResolved is faise, that means ParentLocationBuffer contains multiple + // strings. The use-case for it is recording multiple possible paths for a disk. + // Hyper-V uses this feature to resolve moved disks, which is not typical for our + // use-case. + return OperationResult{E_FAIL, L"Parent virtual disk path resolution failed!"}; + } + } while (!max_depth || --(*max_depth)); + mpl::debug(log_category, + "list_virtual_disk_chain(...) > final chain: {}", + fmt::join(chain, " | --> | ")); + return {NOERROR, L""}; +} + +} // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.h b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.h new file mode 100644 index 00000000000..999fc8b7955 --- /dev/null +++ b/src/platform/backends/hyperv_api/virtdisk/virtdisk_wrapper.h @@ -0,0 +1,113 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include + +#include + +namespace multipass::hyperv::virtdisk +{ + +/** + * A high-level wrapper class that defines + * the common operations that VirtDisk API + * provide. + */ +struct VirtDiskWrapper : public Singleton +{ + /** + * Construct a new VirtDiskWrapper + */ + VirtDiskWrapper(const Singleton::PrivatePass&) noexcept; + + // --------------------------------------------------------- + + /** + * Create a new Virtual Disk + * + * @param [in] params Parameters for the new virtual disk + */ + [[nodiscard]] virtual OperationResult create_virtual_disk( + const CreateVirtualDiskParameters& params) const; + + // --------------------------------------------------------- + + /** + * Resize an existing Virtual Disk + * + * @param [in] vhdx_path Path to the virtual disk + * @param [in] new_size New disk size, in bytes + */ + [[nodiscard]] virtual OperationResult + resize_virtual_disk(const std::filesystem::path& vhdx_path, std::uint64_t new_size_bytes) const; + + // --------------------------------------------------------- + + /** + * Merge a child differencing disk into its parent + * + * @param [in] child Path to the differencing disk + */ + [[nodiscard]] virtual OperationResult merge_virtual_disk_into_parent( + const std::filesystem::path& child) const; + + // --------------------------------------------------------- + + /** + * Reparent a virtual disk + * + * @param [in] child Path to the virtual disk to reparent + * @param [in] parent Path to the new parent + */ + [[nodiscard]] virtual OperationResult reparent_virtual_disk( + const std::filesystem::path& child, + const std::filesystem::path& parent) const; + + // --------------------------------------------------------- + + /** + * Get information about an existing Virtual Disk + * + * @param [in] vhdx_path Path to the virtual disk + * @param [out] vdinfo Virtual disk info output object + */ + [[nodiscard]] virtual OperationResult + get_virtual_disk_info(const std::filesystem::path& vhdx_path, VirtualDiskInfo& vdinfo) const; + + /** + * List all the virtual disks in a virtual disk chain. + * + * @param [in] vhdx_path The chain link to start from + * @param [out] chain The result + * @param [in] max_depth Maximum depth to list (optional) + */ + [[nodiscard]] virtual OperationResult list_virtual_disk_chain( + const std::filesystem::path& vhdx_path, + std::vector& chain, + std::optional max_depth = std::nullopt) const; +}; + +inline const VirtDiskWrapper& VirtDisk() +{ + return VirtDiskWrapper::instance(); +} + +} // namespace multipass::hyperv::virtdisk diff --git a/src/platform/backends/qemu/qemu_virtual_machine.cpp b/src/platform/backends/qemu/qemu_virtual_machine.cpp index 4a1c5ed3284..e26d9a1a33c 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine.cpp @@ -22,6 +22,7 @@ #include "qemu_vm_process_spec.h" #include "qemu_vmstate_process_spec.h" +#include #include #include #include @@ -58,7 +59,6 @@ constexpr auto mount_data_key = "mount_data"; constexpr auto mount_source_key = "source"; constexpr auto mount_arguments_key = "arguments"; -constexpr int shutdown_timeout = 300000; // unit: ms, 5 minute timeout for shutdown/suspend constexpr int kill_process_timeout = 5000; // unit: ms, 5 seconds timeout for killing the process QString get_vm_machine(const boost::json::value& metadata) @@ -380,7 +380,7 @@ void mp::QemuVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { vm_process->write( QByteArray::fromStdString(serialize(qmp_execute_json("system_powerdown")))); - if (vm_process->wait_for_finished(shutdown_timeout)) + if (vm_process->wait_for_finished(vm_shutdown_timeout)) { lock.lock(); state = State::off; @@ -389,7 +389,7 @@ void mp::QemuVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { throw std::runtime_error{fmt::format( "The QEMU process did not finish within {} milliseconds after being shutdown", - shutdown_timeout)}; + vm_shutdown_timeout)}; } } } @@ -409,7 +409,7 @@ void mp::QemuVirtualMachine::suspend() drop_ssh_session(); vm_process->write(QByteArray::fromStdString( serialize(hmc_to_qmp_json(QString{"savevm "} + suspend_tag)))); - vm_process->wait_for_finished(shutdown_timeout); + vm_process->wait_for_finished(vm_shutdown_timeout); vm_process.reset(nullptr); } @@ -427,7 +427,7 @@ mp::VirtualMachine::State mp::QemuVirtualMachine::current_state() int mp::QemuVirtualMachine::ssh_port() { - return 22; + return default_ssh_port; } void mp::QemuVirtualMachine::handle_state_update() diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index 9f66d83869b..43ac23d58cb 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -334,20 +335,19 @@ auto mp::BaseVirtualMachine::get_all_ipv4() -> std::vector return all_ipv4; } -auto mp::BaseVirtualMachine::view_snapshots() const -> SnapshotVista +auto mp::BaseVirtualMachine::view_snapshots(SnapshotPredicate predicate) const -> SnapshotVista { - 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; }); - return ret; -} + SnapshotVista result{}; + for (const auto& [_, snapshot] : snapshots) + { + if (!predicate || predicate(*snapshot)) + result.push_back(snapshot); + } + return result; +} std::shared_ptr mp::BaseVirtualMachine::get_snapshot( const std::string& name) const { diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 88277cb774e..368ced7ecda 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -63,7 +63,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 ad480e8c9bf..84ea03bde49 100644 --- a/src/platform/backends/shared/windows/CMakeLists.txt +++ b/src/platform/backends/shared/windows/CMakeLists.txt @@ -17,7 +17,11 @@ add_library(shared_win STATIC aes.cpp powershell.cpp process_factory.cpp - smb_mount_handler.cpp) + smb_mount_handler.cpp + wsa_init_wrapper.cpp + windows_version.cpp + guid_formatter.cpp + windows_feature_status.cpp) include_directories(shared_win ..) @@ -28,4 +32,5 @@ target_link_libraries(shared_win logger OpenSSL::Crypto sftp_client - utils) + utils + ztd::out_ptr) diff --git a/src/platform/backends/shared/windows/guid_formatter.cpp b/src/platform/backends/shared/windows/guid_formatter.cpp new file mode 100644 index 00000000000..570f6cfbadd --- /dev/null +++ b/src/platform/backends/shared/windows/guid_formatter.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 + +template +template +auto fmt::formatter::format(const GUID& guid, FormatContext& ctx) const + -> FormatContext::iterator +{ + // The format string is laid out char by char to allow it + // to be used for initializing variables with different character + // sizes. + static constexpr Char guid_f[] = { + '{', ':', '0', '8', 'x', '}', '-', '{', ':', '0', '4', 'x', '}', '-', '{', ':', '0', '4', + 'x', '}', '-', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '-', '{', ':', + '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', + '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', '{', ':', '0', '2', 'x', '}', 0}; + return fmt::format_to(ctx.out(), + guid_f, + guid.Data1, + guid.Data2, + guid.Data3, + guid.Data4[0], + guid.Data4[1], + guid.Data4[2], + guid.Data4[3], + guid.Data4[4], + guid.Data4[5], + guid.Data4[6], + guid.Data4[7]); +} + +template auto fmt::formatter::format(const GUID&, + fmt::format_context&) const + -> fmt::format_context::iterator; + +template auto fmt::formatter::format( + const GUID&, + fmt::wformat_context&) const -> fmt::wformat_context::iterator; diff --git a/src/platform/backends/shared/windows/guid_formatter.h b/src/platform/backends/shared/windows/guid_formatter.h new file mode 100644 index 00000000000..a5ad43bcf4d --- /dev/null +++ b/src/platform/backends/shared/windows/guid_formatter.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +struct _GUID; +using GUID = _GUID; + +/** + * Formatter for GUID type + */ +template +struct fmt::formatter : formatter, Char> +{ + template + auto format(const GUID& guid, FormatContext& ctx) const -> FormatContext::iterator; +}; diff --git a/src/platform/backends/shared/windows/smb_mount_handler.cpp b/src/platform/backends/shared/windows/smb_mount_handler.cpp index 0fe1d7b7eaf..2ab3e89a5bb 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 { @@ -62,6 +70,83 @@ catch (const mp::ExitlessSSHProcessException&) mpl::info(category, "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 @@ -82,19 +167,8 @@ void SmbManager::create_share(const QString& share_name, 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"; @@ -102,7 +176,7 @@ void SmbManager::create_share(const QString& share_name, auto wide_source = source.toStdWString(); DWORD parm_err = 0; - SHARE_INFO_2 share_info; + SHARE_INFO_2 share_info = {}; share_info.shi2_netname = wide_share_name.data(); share_info.shi2_remark = remark.data(); share_info.shi2_type = STYPE_DISKTREE; diff --git a/src/platform/backends/shared/windows/wchar_conversion.h b/src/platform/backends/shared/windows/wchar_conversion.h new file mode 100644 index 00000000000..7c93670f26d --- /dev/null +++ b/src/platform/backends/shared/windows/wchar_conversion.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +namespace multipass +{ +std::string wchar_to_utf8(std::wstring_view input); +} diff --git a/src/platform/backends/shared/windows/windows_feature_status.cpp b/src/platform/backends/shared/windows/windows_feature_status.cpp new file mode 100644 index 00000000000..c34de81ecc9 --- /dev/null +++ b/src/platform/backends/shared/windows/windows_feature_status.cpp @@ -0,0 +1,156 @@ +/* + * 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 "windows_feature_status.h" + +#include +#include +#include + +#include +#include + +#include + +#pragma comment(lib, "wbemuuid.lib") + +using ztd::out_ptr::out_ptr; + +namespace +{ +struct ComDeleter +{ + template + void operator()(T* ptr) const + { + if (ptr) + ptr->Release(); + } +}; + +template +using UniqueComPtr = std::unique_ptr; + +struct CoInitGuard +{ + HRESULT hr; + CoInitGuard() : hr(CoInitializeEx(nullptr, COINIT_MULTITHREADED)) + { + } + ~CoInitGuard() + { + if (SUCCEEDED(hr)) + CoUninitialize(); + } + explicit operator bool() const + { + return SUCCEEDED(hr); + } +}; + +struct VariantGuard +{ + VARIANT var; + VariantGuard() + { + VariantInit(&var); + } + ~VariantGuard() + { + VariantClear(&var); + } + VARIANT* operator&() + { + return &var; + } + auto intVal() const + { + return var.intVal; + } +}; + +UniqueComPtr prepare_connection() +{ + UniqueComPtr locator; + if (FAILED(CoCreateInstance(CLSID_WbemLocator, + nullptr, + CLSCTX_INPROC_SERVER, + IID_IWbemLocator, + out_ptr(locator)))) + return nullptr; + + UniqueComPtr services; + if (FAILED(locator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), + nullptr, + nullptr, + nullptr, + 0, + nullptr, + nullptr, + out_ptr(services)))) + return nullptr; + + CoSetProxyBlanket(services.get(), + RPC_C_AUTHN_WINNT, + RPC_C_AUTHZ_NONE, + nullptr, + RPC_C_AUTHN_LEVEL_CALL, + RPC_C_IMP_LEVEL_IMPERSONATE, + nullptr, + EOAC_NONE); + return services; +} +} // namespace + +namespace multipass +{ +std::optional get_windows_feature_state(std::wstring_view feature_name) +{ + thread_local CoInitGuard com_init{}; + + if (!com_init) + return {}; + + const auto query = + fmt::format(L"SELECT InstallState FROM Win32_OptionalFeature WHERE Name='{0}'", + feature_name); + + if (auto conn = prepare_connection()) + { + UniqueComPtr enumerator; + if (FAILED(conn->ExecQuery(_bstr_t(L"WQL"), + _bstr_t(query.c_str()), + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, + nullptr, + out_ptr(enumerator)))) + return {}; + + UniqueComPtr obj; + ULONG count = 0; + if (enumerator->Next(WBEM_INFINITE, 1, out_ptr(obj), &count) != S_OK || !count) + return {}; + + VariantGuard var; + if (FAILED(obj->Get(L"InstallState", 0, &var, nullptr, nullptr))) + return {}; + + return static_cast(var.intVal()); + } + + return {}; +} +} // namespace multipass diff --git a/src/platform/backends/shared/windows/windows_feature_status.h b/src/platform/backends/shared/windows/windows_feature_status.h new file mode 100644 index 00000000000..906f03ffe3d --- /dev/null +++ b/src/platform/backends/shared/windows/windows_feature_status.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +namespace multipass +{ + +enum class WindowsFeatureState : int +{ + Enabled = 1, + Disabled = 2, + Absent = 3, +}; + +/** + * Check if an optional Windows feature is installed. + * + * @param feature_name The feature name, e.g. "VirtualMachinePlatform" + * @return Enum value representing feature's current state, or nullopt on query failure. + */ +std::optional get_windows_feature_state(std::wstring_view feature_name); +} // namespace multipass diff --git a/src/platform/backends/shared/windows/windows_version.cpp b/src/platform/backends/shared/windows/windows_version.cpp new file mode 100644 index 00000000000..1c061751c51 --- /dev/null +++ b/src/platform/backends/shared/windows/windows_version.cpp @@ -0,0 +1,70 @@ +/* + * 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 + +namespace multipass +{ +std::optional get_windows_version() +{ + struct HModuleDeleter + { + void operator()(HMODULE handle) + { + if (handle) + { + FreeLibrary(handle); + } + } + }; + + using unique_hmodule = std::unique_ptr::type, HModuleDeleter>; + using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW); + + static std::optional cached_version = []() -> std::optional { + unique_hmodule hNtdll(LoadLibraryA("ntdll.dll")); + if (hNtdll) + { + RtlGetVersionPtr RtlGetVersion = + (RtlGetVersionPtr)GetProcAddress(hNtdll.get(), "RtlGetVersion"); + if (RtlGetVersion) + { + // Initialize the version info structure + RTL_OSVERSIONINFOW osVersionInfo = {0}; + osVersionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW); + // Call RtlGetVersion + if (const auto status = RtlGetVersion(&osVersionInfo); status == 0) + { + windows_version ver{}; + ver.major = osVersionInfo.dwMajorVersion; + ver.minor = osVersionInfo.dwMinorVersion; + ver.build = osVersionInfo.dwBuildNumber; + return ver; + } + } + } + return {}; + }(); + return cached_version; +} + +} // namespace multipass diff --git a/src/platform/backends/shared/windows/windows_version.h b/src/platform/backends/shared/windows/windows_version.h new file mode 100644 index 00000000000..f006c45d2dd --- /dev/null +++ b/src/platform/backends/shared/windows/windows_version.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +namespace multipass +{ + +struct windows_version +{ + std::uint32_t major; + std::uint32_t minor; + std::uint32_t build; +}; + +std::optional get_windows_version(); + +} // namespace multipass diff --git a/src/platform/backends/shared/windows/wsa_init_wrapper.cpp b/src/platform/backends/shared/windows/wsa_init_wrapper.cpp new file mode 100644 index 00000000000..11494067b7e --- /dev/null +++ b/src/platform/backends/shared/windows/wsa_init_wrapper.cpp @@ -0,0 +1,60 @@ +/* + * 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 mp = multipass; +namespace mpl = mp::logging; +namespace multipass +{ + +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()) + { + throw WSAInitException{" WSAStartup failed with `{}`: {}", + wsa_init_result, + std::system_category().message(wsa_init_result)}; + } +} + +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; +} +} // namespace multipass diff --git a/src/platform/backends/shared/windows/wsa_init_wrapper.h b/src/platform/backends/shared/windows/wsa_init_wrapper.h new file mode 100644 index 00000000000..74703cb1cdc --- /dev/null +++ b/src/platform/backends/shared/windows/wsa_init_wrapper.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 . + * + */ + +#pragma once + +/** + * fwdecl for the Windows Sockets info struct + */ +struct WSAData; + +namespace multipass +{ +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 diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index 941c5e69e60..91c87b1cdbe 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -16,6 +16,7 @@ */ #include +#include #include #include #include @@ -27,11 +28,17 @@ #include #include "backends/hyperv/hyperv_virtual_machine_factory.h" +#if defined(HYPERV_HCS_ENABLED) +#include "backends/hyperv_api/hcs_virtual_machine_factory.h" +#endif #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" #include "shared/windows/process_factory.h" +#include "shared/windows/wchar_conversion.h" +#include "shared/windows/wsa_init_wrapper.h" #include #include @@ -47,13 +54,27 @@ #include +#include +#include #include +#include +#include +#include +#include +#include #include #include + +// clang-format off +#include +#include +// clang-format on #include +#include #include #include +#include #include #include #include @@ -67,7 +88,7 @@ namespace mpu = multipass::utils; namespace { static const auto none = QStringLiteral("none"); -static constexpr auto kLogCategory = "platform-win"; +static constexpr auto log_category = "platform-win"; time_t time_t_from(const FILETIME* ft) { @@ -505,54 +526,279 @@ BOOL signal_handler(DWORD dwCtrlType) return FALSE; } } + +std::string_view adapter_type_to_str(int type) +{ + switch (type) + { + // ipifcons.h + case IF_TYPE_ETHERNET_CSMACD: + return "Ethernet"; + case IF_TYPE_SOFTWARE_LOOPBACK: + return "Loopback"; + case IF_TYPE_IEEE80211: + return "WiFi"; + default: + return "Unknown"; + } +} + +/** + * 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) + const static mp::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 max_prefix_length = 32; + const auto ip_hbo = ntohl(v4.S_un.S_addr); + if (prefix_length > max_prefix_length) + { + 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 max_prefix_length = 128; + if (prefix_length > max_prefix_length) + { + 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::vector unicast_addrs_to_net_addrs( + 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; +} } // namespace +std::string mp::wchar_to_utf8(std::wstring_view input) +{ + 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); + return result; +} + +struct GetNetworkInterfacesInfoException : public multipass::FormattedExceptionBase<> +{ + using multipass::FormattedExceptionBase<>::FormattedExceptionBase; +}; + +struct InvalidNetworkPrefixLengthException : public multipass::FormattedExceptionBase<> +{ + using multipass::FormattedExceptionBase<>::FormattedExceptionBase; +}; + std::map mp::platform::Platform::get_network_interfaces_info() const { - static const auto ps_cmd_base = - QStringLiteral("Get-NetAdapter -physical | Select-Object -Property " - "Name,MediaType,PhysicalMediaType,InterfaceDescription"); - static const auto ps_args = - QString{ps_cmd_base}.split(' ', Qt::SkipEmptyParts) + PowerShell::Snippets::to_bare_csv; + 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)) + 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) { - 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); + // Only list the physical interfaces. + if (!ifRow.InterfaceAndOperStatusFlags.HardwareInterface) + { + continue; + } + + mp::NetworkInterfaceInfo net{}; + net.id = wchar_to_utf8(adapter.FriendlyName); + net.type = adapter_type_to_str(adapter.IfType); + net.description = wchar_to_utf8(adapter.Description); + net.links = unicast_addrs_to_net_addrs(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()) + { + constexpr static auto name_fmtstr = + hyperv::string_literal("vEthernet ({})"); + const std::wstring search = name_fmtstr.format(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_addrs_to_net_addrs(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"; + constexpr std::string_view supported_backends[] = { + "hyperv", + "virtualbox", +#if defined(HYPERV_HCS_ENABLED) + "hyperv_api", +#endif + }; + return std::ranges::any_of(supported_backends, + [&](std::string_view b) { return backend == b; }); } void mp::platform::Platform::set_server_socket_restrictions(const std::string& /* server_address */, @@ -610,7 +856,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 @@ -657,6 +903,12 @@ mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_di return std::make_unique(data_dir); } +#if defined(HYPERV_HCS_ENABLED) + else if (driver == "hyperv_api") + { + return std::make_unique(data_dir); + } +#endif throw std::runtime_error("Invalid virtualization driver set in the environment"); } @@ -685,7 +937,7 @@ mp::UpdatePrompt::UPtr mp::platform::make_update_prompt() int mp::platform::Platform::chown(const char* path, unsigned int uid, unsigned int gid) const { - logging::trace(kLogCategory, + logging::trace(log_category, "chown() called for `{}` (uid: {}, gid: {}) but it's no-op.", path, uid, @@ -891,12 +1143,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/utils/file_ops.cpp b/src/utils/file_ops.cpp index 1abae6b4b77..e080e456e75 100644 --- a/src/utils/file_ops.cpp +++ b/src/utils/file_ops.cpp @@ -383,16 +383,26 @@ void mp::FileOps::copy(const fs::path& src, fs::copy(src, dist, copy_options, ec); } +void mp::FileOps::rename(const fs::path& old_p, const fs::path& new_p) const +{ + fs::rename(old_p, new_p); +} + 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 +bool mp::FileOps::exists(const fs::path& path, std::error_code& err) const noexcept { return fs::exists(path, err); } +bool mp::FileOps::is_symlink(const fs::path& path) const +{ + return fs::is_symlink(path); +} + bool mp::FileOps::is_directory(const fs::path& path, std::error_code& err) const { return fs::is_directory(path, err); @@ -408,7 +418,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/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index cefb4fa8c42..2a0ce95dfdd 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -20,6 +20,21 @@ include(c_mock_defines.cmake) find_package(GTest CONFIG REQUIRED) find_package(premock CONFIG REQUIRED) +############################################################################## +# FIXME: Move unit and integration tests to their separate sub folders +add_executable(multipass_integration_tests) +target_link_libraries(multipass_integration_tests + PRIVATE + GTest::gmock_main + Qt6::Core fmt::fmt-header-only) +target_include_directories(multipass_integration_tests + PRIVATE ${CMAKE_SOURCE_DIR} + PRIVATE ${CMAKE_SOURCE_DIR}/src + PRIVATE ${CMAKE_SOURCE_DIR}/src/platform/backends +) +add_test(NAME multipass_integration_tests COMMAND multipass_integration_tests) +############################################################################## + add_executable(multipass_tests common.cpp daemon_test_fixture.cpp diff --git a/tests/unit/hyperv_api/CMakeLists.txt b/tests/unit/hyperv_api/CMakeLists.txt new file mode 100644 index 00000000000..8561a10f1cc --- /dev/null +++ b/tests/unit/hyperv_api/CMakeLists.txt @@ -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 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 . +# + + +if(WIN32) + target_sources(multipass_tests + PRIVATE + ${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_ut_hyperv_hcs_api.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_request.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_virtdisk.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_virtual_machine.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_ut_hyperv_hcs_virtual_machine_factory.cpp + ) + + target_sources(multipass_integration_tests + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_hcn_api.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_hcs_api.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_it_hyperv_virtdisk.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_bb_cit_hyperv.cpp + ) + + target_link_libraries(multipass_tests hyperv_api_backend) + target_link_libraries(multipass_integration_tests PRIVATE hyperv_api_backend platform) +endif() diff --git a/tests/unit/hyperv_api/hyperv_test_utils.h b/tests/unit/hyperv_api/hyperv_test_utils.h new file mode 100644 index 00000000000..f5da7343bf3 --- /dev/null +++ b/tests/unit/hyperv_api/hyperv_test_utils.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include + +#include + +#define EXPECT_NO_CALL(mock) EXPECT_CALL(mock, Call).Times(0) + +namespace multipass::test +{ + +template +inline auto trim_whitespace(const CharT* 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 + { + + auto_remove_path(std::filesystem::path p) : path(p) + { + } + + ~auto_remove_path() noexcept + { + std::error_code ec{}; + // Use the noexcept overload + std::filesystem::remove(path, ec); + } + + operator const std::filesystem::path&() const& noexcept + { + return path; + } + + operator const std::filesystem::path&() const&& noexcept = delete; + + private: + const std::filesystem::path path; + }; + + std::filesystem::path temp_path{}; + std::uint32_t remaining_attempts = 10; + do + { + 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."}; + } + + return auto_remove_path{temp_path}; +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_hyperv_hcn_api.h b/tests/unit/hyperv_api/mock_hyperv_hcn_api.h new file mode 100644 index 00000000000..ad064de7e05 --- /dev/null +++ b/tests/unit/hyperv_api/mock_hyperv_hcn_api.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 . + * + */ + +#pragma once + +#include + +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ + +class MockHCNAPI : public hyperv::hcn::HCNAPI +{ +public: + using HCNAPI::HCNAPI; + + MOCK_METHOD(HRESULT, + HcnCreateNetwork, + (REFGUID Id, PCWSTR Settings, PHCN_NETWORK Network, PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(HRESULT, + HcnOpenNetwork, + (REFGUID Id, PHCN_NETWORK Network, PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(HRESULT, HcnDeleteNetwork, (REFGUID Id, PWSTR* ErrorRecord), (const override)); + MOCK_METHOD(HRESULT, HcnCloseNetwork, (HCN_NETWORK Network), (const override)); + MOCK_METHOD(HRESULT, + HcnCreateEndpoint, + (HCN_NETWORK Network, + REFGUID Id, + PCWSTR Settings, + PHCN_ENDPOINT Endpoint, + PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(HRESULT, + HcnOpenEndpoint, + (REFGUID Id, PHCN_ENDPOINT Endpoint, PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(HRESULT, HcnDeleteEndpoint, (REFGUID Id, PWSTR* ErrorRecord), (const override)); + MOCK_METHOD(HRESULT, HcnCloseEndpoint, (HCN_ENDPOINT Endpoint), (const override)); + MOCK_METHOD(HRESULT, + HcnEnumerateNetworks, + (PCWSTR Query, PWSTR* Networks, PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(HRESULT, + HcnQueryNetworkProperties, + (HCN_NETWORK Network, PCWSTR Query, PWSTR* Properties, PWSTR* ErrorRecord), + (const override)); + MOCK_METHOD(void, CoTaskMemFree, (LPVOID pv), (const override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockHCNAPI, HCNAPI); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h b/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h new file mode 100644 index 00000000000..81a16b55cfa --- /dev/null +++ b/tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ + +/** + * Mock Host Compute Networking API wrapper for testing. + */ +struct MockHCNWrapper : public hyperv::hcn::HCNWrapper +{ + using HCNWrapper::HCNWrapper; + + MOCK_METHOD(hyperv::OperationResult, + create_network, + (const struct hyperv::hcn::CreateNetworkParameters& params), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + delete_network, + (const std::string& network_guid), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + create_endpoint, + (const struct hyperv::hcn::CreateEndpointParameters& params), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + delete_endpoint, + (const std::string& endpoint_guid), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + enumerate_attached_endpoints, + (const std::string& vm_guid, std::vector& endpoint_guids), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + enumerate_networks, + (std::vector & network_guids), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + query_network, + (const std::string& network_guid, hyperv::hcn::HcnNetworkInfo&), + (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockHCNWrapper, HCNWrapper); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_hyperv_hcs_api.h b/tests/unit/hyperv_api/mock_hyperv_hcs_api.h new file mode 100644 index 00000000000..5023f8bc0a3 --- /dev/null +++ b/tests/unit/hyperv_api/mock_hyperv_hcs_api.h @@ -0,0 +1,132 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ + +class MockHCSAPI : public hyperv::hcs::HCSAPI +{ +public: + using HCSAPI::HCSAPI; + + MOCK_METHOD(HCS_OPERATION, + HcsCreateOperation, + (const void* context, HCS_OPERATION_COMPLETION callback), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsWaitForOperationResult, + (HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument), + (const, override)); + + MOCK_METHOD(void, HcsCloseOperation, (HCS_OPERATION operation), (const, override)); + + MOCK_METHOD(HRESULT, + HcsCreateComputeSystem, + (PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsOpenComputeSystem, + (PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsStartComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsShutDownComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsTerminateComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD(void, HcsCloseComputeSystem, (HCS_SYSTEM computeSystem), (const)); + + MOCK_METHOD(HRESULT, + HcsPauseComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsResumeComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD( + HRESULT, + HcsModifyComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR configuration, HANDLE identity), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsGetComputeSystemProperties, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery), + (const, override)); + + MOCK_METHOD(HRESULT, HcsGrantVmAccess, (PCWSTR vmId, PCWSTR filePath), (const, override)); + + MOCK_METHOD(HRESULT, HcsRevokeVmAccess, (PCWSTR vmId, PCWSTR filePath), (const, override)); + + MOCK_METHOD(HRESULT, + HcsEnumerateComputeSystems, + (PCWSTR query, HCS_OPERATION operation), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsSetComputeSystemCallback, + (HCS_SYSTEM computeSystem, + HCS_EVENT_OPTIONS callbackOptions, + const void* context, + HCS_EVENT_CALLBACK callback), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsSaveComputeSystem, + (HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsCreateEmptyGuestStateFile, + (PCWSTR guestStateFilePath), + (const, override)); + + MOCK_METHOD(HRESULT, + HcsCreateEmptyRuntimeStateFile, + (PCWSTR runtimeStateFilePath), + (const, override)); + + MOCK_METHOD(HLOCAL, LocalFree, (HLOCAL hMem), (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockHCSAPI, HCSAPI); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_hyperv_hcs_wrapper.h b/tests/unit/hyperv_api/mock_hyperv_hcs_wrapper.h new file mode 100644 index 00000000000..9840b25c5ce --- /dev/null +++ b/tests/unit/hyperv_api/mock_hyperv_hcs_wrapper.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 . + * + */ + +#pragma once + +#include + +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ + +/** + * Mock Host Compute System API wrapper for testing. + */ +struct MockHCSWrapper : public hyperv::hcs::HCSWrapper +{ + using HCSWrapper::HCSWrapper; + + MOCK_METHOD(hyperv::OperationResult, + open_compute_system, + (const std::string& compute_system_name, + hyperv::hcs::HcsSystemHandle& out_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + create_compute_system, + (const hyperv::hcs::CreateComputeSystemParameters& params, + hyperv::hcs::HcsSystemHandle& out_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + start_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + shutdown_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + pause_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + resume_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + terminate_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + get_compute_system_properties, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + grant_vm_access, + (const std::string& compute_system_name, const std::filesystem::path& file_path), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + revoke_vm_access, + (const std::string& compute_system_name, const std::filesystem::path& file_path), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + get_compute_system_state, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system, + hyperv::hcs::ComputeSystemState& state_out), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + get_compute_system_guid, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system, std::string& guid_out), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + modify_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system, + const hyperv::hcs::HcsRequest& request), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + set_compute_system_callback, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system, + void* context, + void (*callback)(HCS_EVENT* hcs_event, void* context)), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + save_compute_system, + (const hyperv::hcs::HcsSystemHandle& target_hcs_system, + const hyperv::hcs::HcsPath& save_path), + (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockHCSWrapper, HCSWrapper); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_hyperv_virtdisk_wrapper.h b/tests/unit/hyperv_api/mock_hyperv_virtdisk_wrapper.h new file mode 100644 index 00000000000..4e401b2a840 --- /dev/null +++ b/tests/unit/hyperv_api/mock_hyperv_virtdisk_wrapper.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 . + * + */ + +#pragma once + +#include + +#include "tests/unit/mock_singleton_helpers.h" + +namespace multipass::test +{ + +/** + * Mock virtdisk API wrapper for testing. + */ +struct MockVirtDiskWrapper : public hyperv::virtdisk::VirtDiskWrapper +{ + using VirtDiskWrapper::VirtDiskWrapper; + + MOCK_METHOD(hyperv::OperationResult, + create_virtual_disk, + (const hyperv::virtdisk::CreateVirtualDiskParameters& params), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + resize_virtual_disk, + (const std::filesystem::path& vhdx_path, std::uint64_t new_size_bytes), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + merge_virtual_disk_into_parent, + (const std::filesystem::path& child), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + reparent_virtual_disk, + (const std::filesystem::path& child, const std::filesystem::path& parent), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + get_virtual_disk_info, + (const std::filesystem::path& vhdx_path, hyperv::virtdisk::VirtualDiskInfo& vdinf), + (const, override)); + + MOCK_METHOD(hyperv::OperationResult, + list_virtual_disk_chain, + (const std::filesystem::path& vhdx_path, + std::vector& chain, + std::optional max_depth), + (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockVirtDiskWrapper, VirtDiskWrapper); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_schema_version.h b/tests/unit/hyperv_api/mock_schema_version.h new file mode 100644 index 00000000000..58ac84fdedb --- /dev/null +++ b/tests/unit/hyperv_api/mock_schema_version.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include "../mock_singleton_helpers.h" + +#include + +namespace multipass::test +{ +class MockSchemaUtils : public hyperv::hcs::SchemaUtils +{ +public: + using SchemaUtils::SchemaUtils; + + MOCK_METHOD(hyperv::hcs::HcsSchemaVersion, + get_os_supported_schema_version, + (), + (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockSchemaUtils, hyperv::hcs::SchemaUtils); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/mock_virtdisk_api.h b/tests/unit/hyperv_api/mock_virtdisk_api.h new file mode 100644 index 00000000000..0967901d6e1 --- /dev/null +++ b/tests/unit/hyperv_api/mock_virtdisk_api.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include "../mock_singleton_helpers.h" + +namespace multipass::test +{ + +class MockVirtDiskAPI : public hyperv::virtdisk::VirtDiskAPI +{ +public: + using VirtDiskAPI::VirtDiskAPI; + MOCK_METHOD(DWORD, + CreateVirtualDisk, + (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), + (const, override)); + MOCK_METHOD(DWORD, + OpenVirtualDisk, + (PVIRTUAL_STORAGE_TYPE VirtualStorageType, + PCWSTR Path, + VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask, + OPEN_VIRTUAL_DISK_FLAG Flags, + POPEN_VIRTUAL_DISK_PARAMETERS Parameters, + PHANDLE Handle), + (const, override)); + MOCK_METHOD(DWORD, + ResizeVirtualDisk, + (HANDLE VirtualDiskHandle, + RESIZE_VIRTUAL_DISK_FLAG Flags, + PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped), + (const, override)); + MOCK_METHOD(DWORD, + MergeVirtualDisk, + (HANDLE VirtualDiskHandle, + MERGE_VIRTUAL_DISK_FLAG Flags, + PMERGE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped), + (const, override)); + MOCK_METHOD(DWORD, + GetVirtualDiskInformation, + (HANDLE VirtualDiskHandle, + PULONG VirtualDiskInfoSize, + PGET_VIRTUAL_DISK_INFO VirtualDiskInfo, + PULONG SizeUsed), + (const, override)); + MOCK_METHOD(DWORD, + SetVirtualDiskInformation, + (HANDLE VirtualDiskHandle, PSET_VIRTUAL_DISK_INFO VirtualDiskInfo), + (const, override)); + MOCK_METHOD(BOOL, CloseHandle, (HANDLE hObject), (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockVirtDiskAPI, VirtDiskAPI); +}; +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp new file mode 100644 index 00000000000..b85ac632c7a --- /dev/null +++ b/tests/unit/hyperv_api/test_bb_cit_hyperv.cpp @@ -0,0 +1,277 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_test_utils.h" +#include "tests/unit/common.h" + +#include + +#include +#include +#include +#include +#include + +namespace multipass::test +{ + +using namespace hyperv::hcs; +using hyperv::hcn::HCN; +using hyperv::virtdisk::VirtDisk; + +// 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 +{ +}; + +TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm) +{ + hyperv::hcs::HcsSystemHandle handle{nullptr}; + // 10.0. 0.0 to 10.255. 255.255. + const auto network_parameters = []() { + hyperv::hcn::CreateNetworkParameters network_parameters{}; + network_parameters.name = "multipass-hyperv-cit"; + network_parameters.guid = "b4d77a0e-2507-45f0-99aa-c638f3e47486"; + network_parameters.ipams = { + hyperv::hcn::HcnIpam{hyperv::hcn::HcnIpamType::Static(), + {hyperv::hcn::HcnSubnet{"10.99.99.0/24"}}}}; + return network_parameters; + }(); + + const auto endpoint_parameters = [&network_parameters]() { + hyperv::hcn::CreateEndpointParameters endpoint_parameters{}; + endpoint_parameters.network_guid = network_parameters.guid; + endpoint_parameters.endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029f"; + return endpoint_parameters; + }(); + + const auto temp_path = make_tempfile_path(".vhdx"); + + const hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{ + .size_in_bytes = (1024 * 1024) * 512, // 512 MiB + .path = temp_path, + .predecessor = {}}; + + const auto network_adapter = [&endpoint_parameters]() { + hyperv::hcs::HcsNetworkAdapter network_adapter{}; + network_adapter.endpoint_guid = endpoint_parameters.endpoint_guid; + network_adapter.mac_address = "00-15-5D-9D-CF-69"; + return network_adapter; + }(); + + const auto create_vm_parameters = [&network_adapter]() { + hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; + vm_parameters.name = "multipass-hyperv-cit-vm"; + vm_parameters.processor_count = 1; + vm_parameters.memory_size_mb = 512; + vm_parameters.network_adapters.push_back(network_adapter); + return vm_parameters; + }(); + + if (HCS().open_compute_system(create_vm_parameters.name, handle)) + { + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } + + // Create the test network + { + const auto& [status, status_msg] = HCN().create_network(network_parameters); + ASSERT_TRUE(status.success()); + } + + // Create the test endpoint + { + const auto& [status, status_msg] = HCN().create_endpoint(endpoint_parameters); + ASSERT_TRUE(status.success()); + } + + // Create the test VHDX (empty) + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(create_disk_parameters); + ASSERT_TRUE(status.success()); + } + + // Create test VM + { + const auto& [status, status_msg] = + HCS().create_compute_system(create_vm_parameters, handle); + ASSERT_TRUE(status.success()); + } + + // Start test VM + { + const auto& [status, status_msg] = HCS().start_compute_system(handle); + ASSERT_TRUE(status.success()); + } + + (void)HCS().terminate_compute_system(handle); + handle.reset(); + (void)HCN().delete_endpoint(endpoint_parameters.endpoint_guid); + (void)HCN().delete_network(network_parameters.guid); +} + +TEST_F(HyperV_ComponentIntegrationTests, spawn_empty_test_vm_attach_nic_after_boot) +{ + hyperv::hcs::HcsSystemHandle handle{nullptr}; + // 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 hyperv::virtdisk::CreateVirtualDiskParameters create_disk_parameters{ + .size_in_bytes = (1024 * 1024) * 512, // 512 MiB + .path = temp_path, + .predecessor = {}}; + + const auto create_vm_parameters = []() { + hyperv::hcs::CreateComputeSystemParameters vm_parameters{}; + vm_parameters.name = "multipass-hyperv-cit-vm"; + vm_parameters.processor_count = 1; + vm_parameters.memory_size_mb = 512; + 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)) + { + GTEST_LOG_(WARNING) << "The test endpoint was already present, deleted it."; + } + if (HCN().delete_network(network_parameters.guid)) + { + GTEST_LOG_(WARNING) << "The test network was already present, deleted it."; + } + + if (HCS().open_compute_system(create_vm_parameters.name, handle)) + { + if (HCS().terminate_compute_system(handle)) + { + GTEST_LOG_(WARNING) << "The test system was already present, terminated it."; + } + handle.reset(); + } + } + + // Create the test network + { + const auto& [status, status_msg] = HCN().create_network(network_parameters); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + // Create the test endpoint + { + const auto& [status, status_msg] = HCN().create_endpoint(endpoint_parameters); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + // Create the test VHDX (empty) + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(create_disk_parameters); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + // Create test VM + { + const auto& [status, status_msg] = + HCS().create_compute_system(create_vm_parameters, handle); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + std::string vm_guid{}; + // Start test VM + { + const auto& [status, status_msg] = HCS().start_compute_system(handle); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + ASSERT_TRUE(HCS().get_compute_system_guid(handle, vm_guid)); + ASSERT_FALSE(vm_guid.empty()); + } + + // Add network adapter + { + const HcsRequest add_network_adapter_req{ + HcsResourcePath::NetworkAdapters(network_adapter.endpoint_guid), + HcsRequestType::Add(), + network_adapter}; + const auto& [status, status_msg] = + HCS().modify_compute_system(handle, add_network_adapter_req); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + // Verify that endpoint is attached to the VM + { + // Create another EP so we can ensure that we're only listing the EPs belonging to the VM + { + const auto& [status, status_msg] = + HCN().create_endpoint(hyperv::hcn::CreateEndpointParameters{ + .network_guid = network_parameters.guid, + .endpoint_guid = "aee79cf9-54d1-4653-81fb-8110db97029b", + }); + + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } + + std::vector eps{}; + const auto& [status, status_msg] = HCN().enumerate_attached_endpoints(vm_guid, eps); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + ASSERT_EQ(eps.size(), 1); + ASSERT_EQ(eps[0], network_adapter.endpoint_guid); + } + + EXPECT_TRUE(HCS().terminate_compute_system(handle)) << "Terminate system failed!"; + EXPECT_TRUE(HCN().delete_endpoint(endpoint_parameters.endpoint_guid)) + << "Delete endpoint failed!"; + EXPECT_TRUE(HCN().delete_network(network_parameters.guid)) << "Delete network failed!"; + handle.reset(); +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp new file mode 100644 index 00000000000..e6b45394c28 --- /dev/null +++ b/tests/unit/hyperv_api/test_it_hyperv_hcn_api.cpp @@ -0,0 +1,210 @@ +/* + * 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/unit/common.h" + +#include +#include +#include +#include + +namespace multipass::test +{ + +using namespace hyperv::hcn; +using hyperv::hcn::HCN; + +struct HyperVHCNAPI_IntegrationTests : public ::testing::Test +{ +}; + +TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_network) +{ + CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-create-delete-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + (void)HCN().delete_network(params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().delete_network(params.guid); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +TEST_F(HyperVHCNAPI_IntegrationTests, enumerate_networks) +{ + CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-enumerate-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + (void)HCN().delete_network(params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + } + + { + std::vector guids; + const auto result = HCN().enumerate_networks(guids); + ASSERT_TRUE(result); + EXPECT_NE(std::find(guids.cbegin(), guids.cend(), "b70c479d-f808-4053-aafa-705bc15b6d68"), + guids.cend()); + } + + { + const auto& [status, error_msg] = HCN().delete_network(params.guid); + ASSERT_TRUE(status.success()); + } + + { + std::vector guids; + const auto result = HCN().enumerate_networks(guids); + ASSERT_TRUE(result); + EXPECT_EQ(std::find(guids.cbegin(), guids.cend(), "b70c479d-f808-4053-aafa-705bc15b6d68"), + guids.cend()); + } +} + +TEST_F(HyperVHCNAPI_IntegrationTests, query_network) +{ + CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-query-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + (void)HCN().delete_network(params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + } + + { + HcnNetworkInfo info{}; + const auto result = HCN().query_network("b70c479d-f808-4053-aafa-705bc15b6d68", info); + ASSERT_TRUE(result); + EXPECT_EQ(info.name, params.name); + EXPECT_EQ(info.type, "ICS"); + EXPECT_EQ(info.guid, "b70c479d-f808-4053-aafa-705bc15b6d68"); + } + + { + const auto& [status, error_msg] = HCN().delete_network(params.guid); + ASSERT_TRUE(status.success()); + } +} + +TEST_F(HyperVHCNAPI_IntegrationTests, query_nonexistent_network) +{ + HcnNetworkInfo info{}; + const auto result = HCN().query_network("00000000-0000-0000-0000-000000000000", info); + EXPECT_FALSE(result); +} + +TEST_F(HyperVHCNAPI_IntegrationTests, create_delete_endpoint) +{ + CreateNetworkParameters network_params{}; + network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; + network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + network_params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + CreateEndpointParameters endpoint_params{}; + + endpoint_params.network_guid = network_params.guid; + endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; + + (void)HCN().delete_network(network_params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(network_params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().create_endpoint(endpoint_params); + std::wprintf(L"%s\n", error_msg.c_str()); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().delete_endpoint(endpoint_params.endpoint_guid); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().delete_network(network_params.guid); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +TEST_F(HyperVHCNAPI_IntegrationTests, create_endpoint_explicit_mac) +{ + CreateNetworkParameters network_params{}; + network_params.name = "multipass-hyperv-api-hcn-create-delete-test"; + network_params.guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + network_params.ipams = {HcnIpam{HcnIpamType::Static(), {HcnSubnet{"172.50.224.0/20"}}}}; + + CreateEndpointParameters endpoint_params{}; + + endpoint_params.network_guid = network_params.guid; + endpoint_params.endpoint_guid = "b70c479d-f808-4053-aafa-705bc15b6d70"; + endpoint_params.mac_address = "00-11-22-33-44-55"; + + (void)HCN().delete_network(network_params.guid); + + { + const auto& [status, error_msg] = HCN().create_network(network_params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().create_endpoint(endpoint_params); + std::wprintf(L"%s\n", error_msg.c_str()); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().delete_endpoint(endpoint_params.endpoint_guid); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } + + { + const auto& [status, error_msg] = HCN().delete_network(network_params.guid); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_it_hyperv_hcs_api.cpp b/tests/unit/hyperv_api/test_it_hyperv_hcs_api.cpp new file mode 100644 index 00000000000..66acc7c8e02 --- /dev/null +++ b/tests/unit/hyperv_api/test_it_hyperv_hcs_api.cpp @@ -0,0 +1,331 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_test_utils.h" +#include "multipass/test_data_path.h" +#include "tests/unit/common.h" +#include +#include + +#include + +namespace multipass::test +{ + +using namespace hyperv::hcs; + +struct HyperVHCSAPI_IntegrationTests : public ::testing::Test +{ + constexpr static auto test_vm_name = "mp-hvhcs-4493-9555-b423966e78e7"; + hyperv::hcs::HcsSystemHandle handle{nullptr}; + + std::optional vhdx_path; + std::filesystem::path cloud_init_iso_path{ + fmt::format("{}/cloud-init/cloud-init.iso", test_data_path)}; + + // Something unique to this test. + + void SetUp() override + { + cleanup(); + copy_test_vhdx_for_vm(); + + ASSERT_TRUE(std::filesystem::exists(*vhdx_path)); + ASSERT_TRUE(std::filesystem::exists(cloud_init_iso_path)); + // Numbers are arbitrary -- just check if non-empty., + ASSERT_TRUE(std::filesystem::file_size(*vhdx_path) > 4096); + ASSERT_TRUE(std::filesystem::file_size(cloud_init_iso_path) > 256); + } + + void TearDown() override + { + if (handle) + { + bool called = false; + ASSERT_TRUE(HCS().set_compute_system_callback( + handle, + &called, + [](HCS_EVENT* event, void* context) { + ASSERT_NE(nullptr, event); + ASSERT_NE(nullptr, context); + if (hyperv::hcs::parse_event(event) == hyperv::hcs::HcsEventType::SystemExited) + { + *static_cast(context) = true; + } + })); + + const auto d_result = HCS().terminate_compute_system(handle); + ASSERT_TRUE(d_result); + std::wprintf(L"%s\n\n", d_result.status_msg.c_str()); + handle.reset(); + ASSERT_TRUE(called); + } + } + +private: + std::vector find_split_parts(const std::filesystem::path& dir, + const std::string& prefix) + { + std::vector parts; + for (const auto& entry : std::filesystem::directory_iterator(dir)) + { + if (entry.path().filename().string().starts_with(prefix)) + parts.push_back(entry.path()); + } + std::ranges::sort(parts); // aa < ab < ac lexicographically + return parts; + } + + void merge_files(std::span parts, + const std::filesystem::path& output) + { + std::ofstream out(output, std::ios::binary); + for (const auto& part : parts) + { + std::ifstream in(part, std::ios::binary); + out << in.rdbuf(); + } + } + + void copy_test_vhdx_for_vm() + { + vhdx_path.emplace(make_tempfile_path(".vhdx")); + const auto parts = + find_split_parts(fmt::format("{}/cloud-vhdx", test_data_path), "alpine.vhdx.part-"); + ASSERT_EQ(parts.size(), 3); + merge_files(parts, *vhdx_path); + } + + void cleanup() + { + // Ensure that the test vm does not exists + if (HCS().open_compute_system(test_vm_name, handle)) + (void)HCS().terminate_compute_system(handle); + handle.reset(); + } +}; + +TEST_F(HyperVHCSAPI_IntegrationTests, create_delete_compute_system) +{ + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}, + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}}; + + const auto c_result = HCS().create_compute_system(params, handle); + ASSERT_TRUE(c_result); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::stopped); + + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); +} + +TEST_F(HyperVHCSAPI_IntegrationTests, pause_resume_compute_system) +{ + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}, + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}}; + + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + ASSERT_TRUE(HCS().create_compute_system(params, handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::stopped); + ASSERT_TRUE(HCS().start_compute_system(handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::running); + ASSERT_TRUE(HCS().pause_compute_system(handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::paused); + ASSERT_TRUE(HCS().resume_compute_system(handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::running); +} + +TEST_F(HyperVHCSAPI_IntegrationTests, pause_save_and_resume_compute_system) +{ + const auto temp_savedstate_vmrs_path = make_tempfile_path(".SavedState.vmrs"); + + // Create the compute system, pause and save it to the disk + { + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), + .name = "Primary disk", + .path = *vhdx_path}, + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::Iso(), + .name = "Cloud-init ISO", + .path = cloud_init_iso_path, + .read_only = true}}; + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + ASSERT_TRUE(HCS().create_compute_system(params, handle)); + ASSERT_TRUE(HCS().grant_vm_access(params.name, *vhdx_path)); + ASSERT_TRUE(HCS().grant_vm_access(params.name, cloud_init_iso_path)); + + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::stopped); + ASSERT_TRUE(HCS().start_compute_system(handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::running); + ASSERT_TRUE(HCS().pause_compute_system(handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::paused); + ASSERT_TRUE(HCS().grant_vm_access( + params.name, + static_cast(temp_savedstate_vmrs_path).parent_path())); + ASSERT_TRUE(HCS().save_compute_system(handle, temp_savedstate_vmrs_path)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::paused); + // Terminate the compute system. + ASSERT_TRUE(HCS().terminate_compute_system(handle)); + + handle.reset(); + } + + // Create the compute system again + { + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.guest_state.save_state_file_path = temp_savedstate_vmrs_path; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), + .name = "Primary disk", + .path = *vhdx_path}, + hyperv::hcs::HcsScsiDevice{.type = hyperv::hcs::HcsScsiDeviceType::Iso(), + .name = "Cloud-init ISO", + .path = cloud_init_iso_path, + .read_only = true}}; + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + ASSERT_TRUE(HCS().create_compute_system(params, handle)); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_TRUE(HCS().start_compute_system(handle)); + ASSERT_EQ(state, decltype(state)::stopped); + } +} + +TEST_F(HyperVHCSAPI_IntegrationTests, enumerate_properties) +{ + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}, + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}}; + + const auto c_result = HCS().create_compute_system(params, handle); + + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); + + const auto s_result = HCS().start_compute_system(handle); + ASSERT_TRUE(s_result); + ASSERT_TRUE(s_result.status_msg.empty()); + + const auto p_result = HCS().get_compute_system_properties(handle); + EXPECT_TRUE(p_result); + std::wprintf(L"%s\n", p_result.status_msg.c_str()); +} + +// Later. +// TEST_F(HyperVHCSAPI_IntegrationTests, add_remove_plan9_share) +// { +// hyperv::hcs::CreateComputeSystemParameters params{}; +// params.name = test_vm_name; +// params.memory_size_mb = 1024; +// params.processor_count = 1; +// params.scsi_devices = { +// hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}, +// hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"} + +// }; + +// const auto c_result = HCS().create_compute_system(params, handle); + +// ASSERT_TRUE(c_result); +// ASSERT_TRUE(c_result.status_msg.empty()); + +// const auto s_result = HCS().start_compute_system(handle); +// ASSERT_TRUE(s_result); +// ASSERT_TRUE(s_result.status_msg.empty()); + +// const auto p_result = HCS().get_compute_system_properties(handle); +// EXPECT_TRUE(p_result); +// std::wprintf(L"%s\n", p_result.status_msg.c_str()); + +// const auto add_9p_req = []() { +// hyperv::hcs::HcsAddPlan9ShareParameters share{}; +// share.access_name = test_vm_name; +// share.name = test_vm_name; +// share.host_path = "C://"; +// return hyperv::hcs::HcsRequest{hyperv::hcs::HcsResourcePath::Plan9Shares(), +// hyperv::hcs::HcsRequestType::Add(), +// share}; +// }(); + +// const auto sh_a_result = HCS().modify_compute_system(handle, 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_vm_name; +// share.name = test_vm_name; +// return hyperv::hcs::HcsRequest{hyperv::hcs::HcsResourcePath::Plan9Shares(), +// hyperv::hcs::HcsRequestType::Remove(), +// share}; +// }(); + +// const auto sh_r_result = HCS().modify_compute_system(handle, remove_9p_req); +// EXPECT_TRUE(sh_r_result); +// std::wprintf(L"%s\n", sh_r_result.status_msg.c_str()); +// } + +TEST_F(HyperVHCSAPI_IntegrationTests, instance_with_snapshots) +{ + hyperv::hcs::ComputeSystemState state{hyperv::hcs::ComputeSystemState::unknown}; + + hyperv::hcs::CreateComputeSystemParameters params{}; + params.name = test_vm_name; + params.memory_size_mb = 1024; + params.processor_count = 1; + params.scsi_devices = { + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::Iso(), "cloud-init"}, + hyperv::hcs::HcsScsiDevice{hyperv::hcs::HcsScsiDeviceType::VirtualDisk(), "primary"}}; + + const auto c_result = HCS().create_compute_system(params, handle); + ASSERT_TRUE(HCS().get_compute_system_state(handle, state)); + ASSERT_EQ(state, decltype(state)::stopped); + + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_it_hyperv_virtdisk.cpp b/tests/unit/hyperv_api/test_it_hyperv_virtdisk.cpp new file mode 100644 index 00000000000..a7618e80ffa --- /dev/null +++ b/tests/unit/hyperv_api/test_it_hyperv_virtdisk.cpp @@ -0,0 +1,327 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_test_utils.h" +#include "tests/unit/common.h" + +#include + +#include + +#include +#include + +namespace multipass::test +{ + +using namespace hyperv::virtdisk; + +struct HyperVVirtDisk_IntegrationTests : public ::testing::Test +{ + /** + * 16 MiB + */ + constexpr static auto test_vhdx_size = 1024 * 1024 * 16ULL; +}; + +TEST_F(HyperVVirtDisk_IntegrationTests, create_virtual_disk_vhdx) +{ + auto temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Path: %s\n", static_cast(temp_path).c_str()); + + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); +} + +TEST_F(HyperVVirtDisk_IntegrationTests, create_virtual_disk_vhd) +{ + auto temp_path = make_tempfile_path(".vhd"); + std::wprintf(L"Path: %s\n", static_cast(temp_path).c_str()); + + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); +} + +TEST_F(HyperVVirtDisk_IntegrationTests, get_virtual_disk_properties) +{ + auto temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Path: %s\n", static_cast(temp_path).c_str()); + + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = temp_path, + .predecessor = {}}; + + const auto c_result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); + + VirtualDiskInfo info{}; + const auto g_result = VirtDisk().get_virtual_disk_info(temp_path, info); + + 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); +} + +TEST_F(HyperVVirtDisk_IntegrationTests, resize_grow) +{ + auto temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Path: %s\n", static_cast(temp_path).c_str()); + + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = temp_path, + .predecessor = {}}; + + const auto c_result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(c_result); + ASSERT_TRUE(c_result.status_msg.empty()); + + VirtualDiskInfo info{}; + const auto g_result = VirtDisk().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 = VirtDisk().resize_virtual_disk(temp_path, params.size_in_bytes * 2); + ASSERT_TRUE(r_result); + + info = {}; + + const auto g2_result = VirtDisk().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()); + + 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); + + fmt::print("{}", info); +} + +TEST_F(HyperVVirtDisk_IntegrationTests, create_child_disk) +{ + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", + static_cast(parent_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = parent_temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().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()); + { + + const CreateVirtualDiskParameters params{.size_in_bytes = 0, + .path = child_temp_path, + .predecessor = + ParentPathParameters{parent_temp_path}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } +} + +TEST_F(HyperVVirtDisk_IntegrationTests, merge_virtual_disk) +{ + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", + static_cast(parent_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = parent_temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().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()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = 0, + .path = child_temp_path, + .predecessor = + ParentPathParameters{parent_temp_path}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Merge child to parent + const auto result = VirtDisk().merge_virtual_disk_into_parent(child_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); +} + +TEST_F(HyperVVirtDisk_IntegrationTests, merge_reparent_virtual_disk) +{ + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", + static_cast(parent_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = parent_temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().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()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = 0, + .path = child_temp_path, + .predecessor = + ParentPathParameters{parent_temp_path}}; + + const auto result = VirtDisk().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()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = 0, + .path = grandchild_temp_path, + .predecessor = + ParentPathParameters{child_temp_path}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Merge child to parent + { + const auto result = VirtDisk().merge_virtual_disk_into_parent(child_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Reparent grandchild to parent + { + const auto result = + VirtDisk().reparent_virtual_disk(grandchild_temp_path, parent_temp_path); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } +} + +TEST_F(HyperVVirtDisk_IntegrationTests, list_parents) +{ + // Create parent + auto parent_temp_path = make_tempfile_path(".vhdx"); + std::wprintf(L"Parent Path: %s\n", + static_cast(parent_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = parent_temp_path, + .predecessor = {}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + // Create child #1 + auto child1_temp_path = make_tempfile_path(".avhdx"); + + std::wprintf(L"Child Path: %s\n", static_cast(child1_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = child1_temp_path, + .predecessor = + ParentPathParameters{parent_temp_path}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Create child #2 + auto child2_temp_path = make_tempfile_path(".avhdx"); + std::wprintf(L"Child Path: %s\n", static_cast(child2_temp_path).c_str()); + { + const CreateVirtualDiskParameters params{.size_in_bytes = test_vhdx_size, + .path = child2_temp_path, + .predecessor = + ParentPathParameters{child1_temp_path}}; + + const auto result = VirtDisk().create_virtual_disk(params); + ASSERT_TRUE(result); + ASSERT_TRUE(result.status_msg.empty()); + } + + // Try to list + std::vector result{}; + ASSERT_TRUE(VirtDisk().list_virtual_disk_chain(child2_temp_path, result)); + ASSERT_EQ(result.size(), 3); + + EXPECT_TRUE(std::filesystem::equivalent(result[0], child2_temp_path)); + EXPECT_TRUE(std::filesystem::equivalent(result[1], child1_temp_path)); + EXPECT_TRUE(std::filesystem::equivalent(result[2], parent_temp_path)); + + for (const auto& path : result) + { + std::wprintf(L"Child Path: %s\n", path.c_str()); + } +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp new file mode 100644 index 00000000000..a237635c568 --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcn_api.cpp @@ -0,0 +1,767 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "hyperv_test_utils.h" +#include "tests/unit/common.h" +#include "tests/unit/hyperv_api/mock_hyperv_hcn_api.h" +#include "tests/unit/mock_logger.h" + +#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; +using testing::StrictMock; + +namespace multipass::test +{ + +using hcn::HCN; + +struct HyperVHCNAPI_UnitTests : public ::testing::Test +{ + + mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); + + mpt::MockHCNAPI::GuardedMock mock_hcn_api_injection = mpt::MockHCNAPI::inject(); + mpt::MockHCNAPI& mock_hcn_api = *mock_hcn_api_injection.first; + + // 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_network_object = reinterpret_cast(0xbadf00d); + inline static auto mock_endpoint_object = reinterpret_cast(0xbadcafe); + + // Generic error message for all tests, intended to be used for API calls returning + // an "error_record". + inline static wchar_t mock_error_msg[16] = L"It's a failure."; +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_ics) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .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": "ICS", + "Ipams": [ + { + "Type": "static", + "Subnets": [ + { + "Policies": [], + "Routes": [], + "IpAddressPrefix": "172.50.224.0/20", + "IpSubnets": null + } + ] + } + ], + "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_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + { // Verify the expected outcome. + hcn::CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = { + hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; + + const auto& [status, status_msg] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_transparent) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .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_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + { // Verify the expected outcome. + 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] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_with_flags_multiple_policies) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .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_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + { // Verify the expected outcome. + 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] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_success_multiple_ipams) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .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_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + } + + { // Verify the expected outcome. + 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] = HCN().create_network(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario 2: HcnCloseNetwork returns an error. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_close_network_failed) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .WillOnce(DoAll( + [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { + *network = mock_network_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnCloseNetwork) + .WillOnce(DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, + Return(E_POINTER))); + + logger_scope.mock_logger->expect_log(mpl::Level::trace, "HCNWrapper::create_network(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "perform_hcn_operation(...)"); + } + + { // Verify the expected outcome. + hcn::CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = { + hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; + + const auto& [success, error_msg] = HCN().create_network(params); + ASSERT_TRUE(success.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Failure scenario 1: HcnCreateNetwork returns an error. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_network_failed) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateNetwork) + .WillOnce(DoAll( + [&](REFGUID id, PCWSTR settings, PHCN_NETWORK network, PWSTR* error_record) { + *network = mock_network_object; + *error_record = mock_error_msg; + }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, CoTaskMemFree).WillOnce([&](void* ptr) { + EXPECT_EQ(ptr, mock_error_msg); + }); + + logger_scope.mock_logger->expect_log(mpl::Level::trace, "HCNWrapper::create_network(...)"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, "perform_hcn_operation(...)"); + } + + { // Verify the expected outcome. + hcn::CreateNetworkParameters params{}; + params.name = "multipass-hyperv-api-hcn-create-test"; + params.guid = "{b70c479d-f808-4053-aafa-705bc15b6d68}"; + params.ipams = { + hcn::HcnIpam{hcn::HcnIpamType::Static(), {hcn::HcnSubnet{"172.50.224.0/20"}}}}; + + const auto& [status, error_msg] = HCN().create_network(params); + ASSERT_FALSE(status.success()); + ASSERT_EQ(static_cast(status), E_POINTER); + ASSERT_FALSE(error_msg.empty()); + ASSERT_STREQ(error_msg.c_str(), mock_error_msg); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, delete_network_success) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnDeleteNetwork) + .WillOnce(DoAll( + [&](REFGUID guid, PWSTR* error_record) { + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(guid)); + ASSERT_EQ(nullptr, *error_record); + ASSERT_NE(nullptr, error_record); + }, + Return(NOERROR))); + + // Expected logs + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::delete_network(...) > network_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + } + + { // Verify the expected outcome. + const auto& [status, error_msg] = + HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Failure scenario: API call returns non-success + */ +TEST_F(HyperVHCNAPI_UnitTests, delete_network_failed) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnDeleteNetwork) + .WillOnce(DoAll( + [&](REFGUID, PWSTR* error_record) { + ASSERT_EQ(nullptr, *error_record); + ASSERT_NE(nullptr, error_record); + *error_record = mock_error_msg; + }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcn_api, CoTaskMemFree).WillOnce([&](void* ptr) { + EXPECT_EQ(ptr, mock_error_msg); + }); + // Expected logs + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "HCNWrapper::delete_network(...) > network_guid: af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); + } + + { // Verify the expected outcome. + const auto& [status, error_msg] = + HCN().delete_network("af3fb745-2f23-463c-8ded-443f876d9e81"); + ASSERT_FALSE(status.success()); + ASSERT_FALSE(error_msg.empty()); + ASSERT_STREQ(error_msg.c_str(), mock_error_msg); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_success) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateEndpoint) + .WillOnce(DoAll( + [&](HCN_NETWORK network, + REFGUID id, + PCWSTR settings, + PHCN_ENDPOINT endpoint, + PWSTR* error_record) { + constexpr auto expected_endpoint_settings = LR"""( + { + "SchemaVersion": { + "Major": 2, + "Minor": 16 + }, + "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", + "Policies": [ + ], + "MacAddress": null + })"""; + + ASSERT_NE(nullptr, network); + ASSERT_EQ(mock_network_object, network); + ASSERT_NE(nullptr, error_record); + ASSERT_EQ(nullptr, *error_record); + ASSERT_NE(nullptr, endpoint); + ASSERT_EQ(nullptr, *endpoint); + 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()); + ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", fmt::to_string(id)); + *endpoint = mock_endpoint_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint) + .WillOnce(DoAll([&](HCN_ENDPOINT n) { ASSERT_EQ(n, mock_endpoint_object); }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnOpenNetwork) + .WillOnce(DoAll( + [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { + ASSERT_EQ("b70c479d-f808-4053-aafa-705bc15b6d68", fmt::to_string(id)); + ASSERT_NE(nullptr, network); + ASSERT_EQ(nullptr, *network); + ASSERT_NE(nullptr, error_record); + ASSERT_EQ(nullptr, *error_record); + *network = mock_network_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::create_endpoint(...) > params: "); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true", + testing::Exactly(2)); + } + + { // Verify the expected outcome. + hcn::CreateEndpointParameters params{}; + params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; + params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + + const auto& [status, error_msg] = HCN().create_endpoint(params); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Failure scenario: internal open_network call fails. + */ +TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_open_network_failed) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnOpenNetwork).WillOnce(Return(E_POINTER)); + + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::create_endpoint(...) > params: "); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); + } + + { // Verify the expected outcome. + hcn::CreateEndpointParameters params{}; + params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; + params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + + const auto& [status, error_msg] = HCN().create_endpoint(params); + ASSERT_FALSE(status.success()); + ASSERT_EQ(E_POINTER, static_cast(status)); + ASSERT_TRUE(error_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCNAPI_UnitTests, create_endpoint_failure) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnCreateEndpoint) + .WillOnce(DoAll( + [&](HCN_NETWORK network, + REFGUID id, + PCWSTR settings, + PHCN_ENDPOINT endpoint, + PWSTR* error_record) { + constexpr auto expected_endpoint_settings = LR"""( + { + "SchemaVersion": { + "Major": 2, + "Minor": 16 + }, + "HostComputeNetwork": "b70c479d-f808-4053-aafa-705bc15b6d68", + "Policies": [ + ], + "MacAddress": null + })"""; + + ASSERT_EQ(mock_network_object, network); + ASSERT_NE(nullptr, error_record); + 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()); + ASSERT_EQ("77c27c1e-8204-437d-a7cc-fb4ce1614819", fmt::to_string(id)); + *endpoint = mock_endpoint_object; + *error_record = mock_error_msg; + }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcn_api, HcnCloseEndpoint) + .WillOnce(DoAll([&](HCN_ENDPOINT n) { ASSERT_EQ(n, mock_endpoint_object); }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnOpenNetwork) + .WillOnce(DoAll( + [&](REFGUID id, PHCN_NETWORK network, PWSTR* error_record) { + 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; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, HcnCloseNetwork) + .WillOnce( + DoAll([&](HCN_NETWORK n) { ASSERT_EQ(n, mock_network_object); }, Return(NOERROR))); + + EXPECT_CALL(mock_hcn_api, CoTaskMemFree).WillOnce([](const void* ptr) { + ASSERT_EQ(ptr, mock_error_msg); + }); + + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::create_endpoint(...) > params: "); + logger_scope.mock_logger->expect_log( + mpl::Level::trace, + "open_network(...) > network_guid: b70c479d-f808-4053-aafa-705bc15b6d68"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); + } + + { // Verify the expected outcome. + hcn::CreateEndpointParameters params{}; + params.endpoint_guid = "77c27c1e-8204-437d-a7cc-fb4ce1614819"; + params.network_guid = "b70c479d-f808-4053-aafa-705bc15b6d68"; + + const auto& [status, error_msg] = HCN().create_endpoint(params); + ASSERT_FALSE(status.success()); + ASSERT_FALSE(error_msg.empty()); + ASSERT_STREQ(error_msg.c_str(), mock_error_msg); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_success) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnDeleteEndpoint) + .WillOnce(DoAll( + [&](REFGUID guid, PWSTR* error_record) { + ASSERT_EQ("af3fb745-2f23-463c-8ded-443f876d9e81", fmt::to_string(guid)); + ASSERT_EQ(nullptr, *error_record); + ASSERT_NE(nullptr, error_record); + }, + Return(NOERROR))); + + // Expected logs + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::delete_endpoint(...) > endpoint_guid: " + "af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: true"); + } + + { // Verify the expected outcome. + const auto& [status, error_msg] = + HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(error_msg.empty()); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCNAPI_UnitTests, delete_endpoint_failure) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcn_api, HcnDeleteEndpoint) + .WillOnce(DoAll([&](REFGUID, PWSTR* error_record) { *error_record = mock_error_msg; }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcn_api, CoTaskMemFree).WillOnce([](const void* ptr) { + ASSERT_EQ(ptr, mock_error_msg); + }); + + // Expected logs + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "HCNWrapper::delete_endpoint(...) > endpoint_guid: " + "af3fb745-2f23-463c-8ded-443f876d9e81"); + logger_scope.mock_logger->expect_log(mpl::Level::trace, + "perform_hcn_operation(...) > result: false"); + } + + { // Verify the expected outcome. + const auto& [status, error_msg] = + HCN().delete_endpoint("af3fb745-2f23-463c-8ded-443f876d9e81"); + ASSERT_FALSE(status.success()); + ASSERT_FALSE(error_msg.empty()); + ASSERT_STREQ(error_msg.c_str(), mock_error_msg); + } +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcn_ipam.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_ipam.cpp new file mode 100644 index 00000000000..ef9ce0cd0ba --- /dev/null +++ b/tests/unit/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/unit/common.h" +#include "tests/unit/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/unit/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_network_policy.cpp new file mode 100644 index 00000000000..bf07625b200 --- /dev/null +++ b/tests/unit/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/unit/common.h" +#include "tests/unit/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/unit/hyperv_api/test_ut_hyperv_hcn_route.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_route.cpp new file mode 100644 index 00000000000..4e6bfbf3e2f --- /dev/null +++ b/tests/unit/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/unit/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/unit/hyperv_api/test_ut_hyperv_hcn_subnet.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcn_subnet.cpp new file mode 100644 index 00000000000..4b88f567c68 --- /dev/null +++ b/tests/unit/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/unit/common.h" +#include "tests/unit/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/unit/hyperv_api/test_ut_hyperv_hcs_api.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_api.cpp new file mode 100644 index 00000000000..2f50974cb20 --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_api.cpp @@ -0,0 +1,2536 @@ +/* + * 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 "gmock/gmock.h" +#include "hyperv_api/hcs/hyperv_hcs_create_compute_system_params.h" +#include "hyperv_api/hcs/hyperv_hcs_wrapper.h" +#include "hyperv_test_utils.h" +#include "mock_hyperv_hcs_api.h" +#include "mock_schema_version.h" +#include "tests/unit/mock_logger.h" + +#include +#include +#include +#include +#include + +namespace mpt = multipass::test; +namespace mpl = multipass::logging; + +using testing::DoAll; +using testing::Eq; +using testing::NiceMock; +using testing::Return; +using testing::StrEq; +using testing::StrictMock; + +namespace multipass::test +{ +using namespace hyperv::hcs; + +struct HyperVHCSAPI_UnitTests : public ::testing::Test +{ + mpt::MockLogger::Scope logger_scope = [] { + auto v = mpt::MockLogger::inject(); + v.mock_logger->screen_logs(mpl::Level::info); + return v; + }(); + + const mpt::MockSchemaUtils::GuardedMock mock_schema_utils_injection = + mpt::MockSchemaUtils::inject(); + + mpt::MockHCSAPI::GuardedMock mock_hcs_api_injection = mpt::MockHCSAPI::inject(); + mpt::MockHCSAPI& mock_hcs_api = *mock_hcs_api_injection.first; + + void SetUp() override + { + // Use the most extensive version by default. + ON_CALL(*mock_schema_utils_injection.first, get_os_supported_schema_version()) + .WillByDefault(Return(HcsSchemaVersion::v26)); + } + + // 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_operation_object = reinterpret_cast(0xbadf00d); + inline static auto mock_compute_system_object = reinterpret_cast(0xbadcafe); + + // Generic error message for all tests, intended to be used for API calls returning + // an "error_record". + inline static wchar_t mock_error_msg[16] = L"It's a failure."; + inline static wchar_t mock_success_msg[16] = L"Succeeded."; + inline static wchar_t operation_fail_msg[22] = L"HCS operation failed!"; + inline static wchar_t hcs_create_operation_fail_msg[27] = L"HcsCreateOperation failed!"; + inline static wchar_t hcs_open_compute_system_fail_msg[29] = L"HcsOpenComputeSystem failed!"; + + template + void generic_operation_happy_path(UutCallableT uut_callback, + PWSTR operation_result_document = nullptr, + PWSTR expected_status_msg = nullptr); + + template + void generic_operation_fail(UutCallableT uut_callback, + PWSTR expected_status_msg = operation_fail_msg); + + template + void generic_operation_wait_for_operation_fail(UutCallableT uut_callback, + PWSTR operation_result_document = mock_error_msg, + PWSTR expected_status_msg = mock_error_msg); + + template + void generic_operation_hcs_open_fail(UutCallableT uut_callback, + PWSTR expected_status_msg = hcs_create_operation_fail_msg); + + template + void generic_operation_create_operation_fail( + UutCallableT uut_callback, + PWSTR expected_status_msg = hcs_create_operation_fail_msg); + + void verify_create_compute_system_failure(const CreateComputeSystemParameters& params) + { + HcsSystemHandle handle{nullptr}; + const auto& [status, status_msg] = HCS().create_compute_system(params, handle); + ASSERT_FALSE(status.success()); + ASSERT_EQ(nullptr, handle); + ASSERT_TRUE(status_msg.empty()); + } + + void verify_create_compute_system_success(const CreateComputeSystemParameters& params) + { + HcsSystemHandle handle{nullptr}; + const auto& [status, status_msg] = HCS().create_compute_system(params, handle); + ASSERT_TRUE(status.success()); + ASSERT_NE(nullptr, handle); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), mock_success_msg); + } +}; + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_happy_path) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": { + "cloud-init": { + "Attachments": { + "0": { + "Type": "Iso", + "Path": "cloudinit iso path", + "ReadOnly": true + } + } + }, + "primary": { + "Attachments": { + "0": { + "Type": "VirtualDisk", + "Path": "virtual disk path", + "ReadOnly": false + } + } + } + }, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + }, + "GuestState": { + "GuestStateFilePath": "non-empty.vmgs", + "RuntimeStateFilePath": "non-empty.vmrs" + }, + "RestoreState": { + "SaveStateFilePath": "non-empty.savedstate.vmrs" + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [](HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = mock_success_msg; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + EXPECT_CALL(mock_hcs_api, HcsCreateEmptyGuestStateFile(StrEq(L"non-empty.vmgs"))) + .WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, HcsCreateEmptyRuntimeStateFile(StrEq(L"non-empty.vmrs"))) + .WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, HcsGrantVmAccess(StrEq(L"test_vm"), StrEq(L"non-empty.vmrs"))) + .WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, HcsGrantVmAccess(StrEq(L"test_vm"), StrEq(L"non-empty.vmgs"))) + .WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, LocalFree(Eq(mock_success_msg))).WillOnce(Return(nullptr)); + } + + { // Verify the expected outcome. + verify_create_compute_system_success({ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::Iso(), "cloud-init", "cloudinit iso path", true}, + {HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}, + }, + .guest_state = + { + .guest_state_file_path = "non-empty.vmgs", + .runtime_state_file_path = "non-empty.vmrs", + .save_state_file_path = "non-empty.savedstate.vmrs", + }, + }); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_vmgs_create_fail) +{ + { // Verify that the dependencies are called with right data + EXPECT_CALL(mock_hcs_api, HcsCreateEmptyGuestStateFile(StrEq(L"non-empty.vmgs"))) + .WillOnce(Return(E_POINTER)); + } + + { // Verify the expected outcome. + verify_create_compute_system_failure({ + .name = "test_vm", + .guest_state = + { + .guest_state_file_path = "non-empty.vmgs", + .runtime_state_file_path = "non-empty.vmrs", + .save_state_file_path = "non-empty.savedstate.vmrs", + }, + }); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_vmrs_create_fail) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateEmptyGuestStateFile(StrEq(L"non-empty.vmgs"))) + .WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, HcsGrantVmAccess).WillOnce(Return(NOERROR)); + + EXPECT_CALL(mock_hcs_api, HcsCreateEmptyRuntimeStateFile(StrEq(L"non-empty.vmrs"))) + .WillOnce(Return(E_POINTER)); + } + + { // Verify the expected outcome. + verify_create_compute_system_failure({ + .name = "test_vm", + .guest_state = + { + .guest_state_file_path = "non-empty.vmgs", + .runtime_state_file_path = "non-empty.vmrs", + .save_state_file_path = "non-empty.savedstate.vmrs", + }, + }); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": { + "primary": { + "Attachments": { + "0": { + "Type": "VirtualDisk", + "Path": "virtual disk path", + "ReadOnly": false + } + } + } + }, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [](HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = mock_success_msg; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); + } + + { // Verify the expected outcome. + verify_create_compute_system_success({ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}, + }, + }); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_vhdx) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": { + "cloud-init": { + "Attachments": { + "0": { + "Type": "Iso", + "Path": "cloudinit iso path", + "ReadOnly": true + } + } + } + }, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [](HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = mock_success_msg; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); + } + + { // Verify the expected outcome. + verify_create_compute_system_success({ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::Iso(), "cloud-init", "cloudinit iso path", true}, + }, + }); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wo_cloudinit_and_vhdx) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": {}, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [](HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = mock_success_msg; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_success_msg); }, Return(nullptr))); + } + + { // Verify the expected outcome. + verify_create_compute_system_success({ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + }); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_create_operation_fail) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(nullptr))); + } + + { // Verify the expected outcome. + HcsSystemHandle handle{nullptr}; + const CreateComputeSystemParameters params{ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::Iso(), "cloud-init", "cloudinit iso path", true}, + {HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}, + }, + }; + + const auto& [status, status_msg] = HCS().create_compute_system(params, handle); + ASSERT_FALSE(status.success()); + ASSERT_EQ(nullptr, handle); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"HcsCreateOperation failed."); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_fail) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": { + "cloud-init": { + "Attachments": { + "0": { + "Type": "Iso", + "Path": "cloudinit iso path", + "ReadOnly": true + } + } + }, + "primary": { + "Attachments": { + "0": { + "Type": "VirtualDisk", + "Path": "virtual disk path", + "ReadOnly": false + } + } + } + }, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + }, + Return(E_POINTER))); + } + + { // Verify the expected outcome. + HcsSystemHandle handle{nullptr}; + const CreateComputeSystemParameters params{ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::Iso(), "cloud-init", "cloudinit iso path", true}, + {HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}, + }, + }; + + const auto& [status, status_msg] = HCS().create_compute_system(params, handle); + ASSERT_FALSE(status.success()); + ASSERT_EQ(nullptr, handle); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"HcsCreateComputeSystem failed."); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, create_compute_system_wait_for_operation_fail) +{ + constexpr auto expected_vm_settings_json = 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": 16384 + }, + "Processor": { + "Count": 8 + } + }, + "Devices": { + "ComPorts": { + "0": { + "NamedPipe": "\\\\.\\pipe\\test_vm" + } + }, + "Scsi": { + "cloud-init": { + "Attachments": { + "0": { + "Type": "Iso", + "Path": "cloudinit iso path", + "ReadOnly": true + } + } + }, + "primary": { + "Attachments": { + "0": { + "Type": "VirtualDisk", + "Path": "virtual disk path", + "ReadOnly": false + } + } + } + }, + "NetworkAdapters": {} + }, + "Services": { + "Shutdown": {}, + "Heartbeat": {} + } + } + })"; + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [](HCS_OPERATION operation, DWORD timeoutMs, PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = mock_error_msg; + }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcs_api, HcsCreateComputeSystem) + .WillOnce(DoAll( + [](PCWSTR id, + PCWSTR configuration, + HCS_OPERATION operation, + const SECURITY_DESCRIPTOR* securityDescriptor, + HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(L"test_vm", id); + + const auto config_no_whitespace = trim_whitespace(configuration); + const auto expected_no_whitespace = trim_whitespace(expected_vm_settings_json); + + ASSERT_STREQ(expected_no_whitespace.c_str(), config_no_whitespace.c_str()); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, securityDescriptor); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([](HLOCAL ptr) { ASSERT_EQ(ptr, mock_error_msg); }, Return(nullptr))); + } + + { // Verify the expected outcome. + HcsSystemHandle handle{nullptr}; + const CreateComputeSystemParameters params{ + .name = "test_vm", + .memory_size_mb = 16384, + .processor_count = 8, + .scsi_devices = + { + {HcsScsiDeviceType::Iso(), "cloud-init", "cloudinit iso path", true}, + {HcsScsiDeviceType::VirtualDisk(), "primary", "virtual disk path"}, + }, + }; + + const auto& [status, status_msg] = HCS().create_compute_system(params, handle); + ASSERT_FALSE(status.success()); + ASSERT_EQ(nullptr, handle); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), mock_error_msg); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, grant_vm_access_success) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsGrantVmAccess) + .WillOnce(DoAll( + [](PCWSTR vmId, PCWSTR filePath) { + ASSERT_NE(nullptr, vmId); + ASSERT_NE(nullptr, filePath); + ASSERT_STREQ(vmId, L"test_vm"); + ASSERT_STREQ(filePath, L"this is a path"); + }, + Return(NOERROR))); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = HCS().grant_vm_access("test_vm", "this is a path"); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, grant_vm_access_fail) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsGrantVmAccess) + .WillOnce(DoAll( + [](PCWSTR vmId, PCWSTR filePath) { + ASSERT_NE(nullptr, vmId); + ASSERT_NE(nullptr, filePath); + ASSERT_STREQ(vmId, L"test_vm"); + ASSERT_STREQ(filePath, L"this is a path"); + }, + Return(E_POINTER))); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = HCS().grant_vm_access("test_vm", "this is a path"); + ASSERT_FALSE(status.success()); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"GrantVmAccess failed!"); + } +} + +// --------------------------------------------------------- + +/** + * Success scenario: Everything goes as expected. + */ +TEST_F(HyperVHCSAPI_UnitTests, revoke_vm_access_success) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsRevokeVmAccess) + .WillOnce(DoAll( + [](PCWSTR vmId, PCWSTR filePath) { + ASSERT_NE(nullptr, vmId); + ASSERT_NE(nullptr, filePath); + ASSERT_STREQ(vmId, L"test_vm"); + ASSERT_STREQ(filePath, L"this is a path"); + }, + Return(NOERROR))); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = HCS().revoke_vm_access("test_vm", "this is a path"); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, revoke_vm_access_fail) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsRevokeVmAccess) + .WillOnce(DoAll( + [](PCWSTR vmId, PCWSTR filePath) { + ASSERT_NE(nullptr, vmId); + ASSERT_NE(nullptr, filePath); + ASSERT_STREQ(vmId, L"test_vm"); + ASSERT_STREQ(filePath, L"this is a path"); + }, + Return(E_POINTER))); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = HCS().revoke_vm_access("test_vm", "this is a path"); + ASSERT_FALSE(status.success()); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"RevokeVmAccess failed!"); + } +} + +// --------------------------------------------------------- + +// +// Below are the skeleton test cases for the functions that are following +// the same pattern. +// + +template +void HyperVHCSAPI_UnitTests::generic_operation_happy_path(UutCallableT uut_callback, + PWSTR operation_result_document, + PWSTR expected_status_msg) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [operation_result_document](HCS_OPERATION operation, + DWORD timeoutMs, + PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = operation_result_document; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsOpenComputeSystem) + .WillOnce(DoAll( + [&](PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(id, L"test_vm"); + ASSERT_EQ(requestedAccess, GENERIC_ALL); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + if (operation_result_document) + { + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([operation_result_document]( + HLOCAL ptr) { ASSERT_EQ(operation_result_document, ptr); }, + Return(nullptr))); + } + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = uut_callback(); + ASSERT_TRUE(status.success()); + + if (nullptr == expected_status_msg) + { + ASSERT_TRUE(status_msg.empty()); + } + else + { + ASSERT_STREQ(status_msg.c_str(), expected_status_msg); + } + } +} + +template +void HyperVHCSAPI_UnitTests::generic_operation_hcs_open_fail(UutCallableT uut_callback, + PWSTR expected_status_msg) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsOpenComputeSystem) + .WillOnce(DoAll( + [&](PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(id, L"test_vm"); + ASSERT_EQ(requestedAccess, GENERIC_ALL); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + }, + Return(E_POINTER))); + + logger_scope.mock_logger->expect_log( + mpl::Level::error, + "perform_hcs_operation(...) > Host Compute System handle is null!"); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = uut_callback(); + ASSERT_FALSE(status.success()); + + if (nullptr == expected_status_msg) + { + ASSERT_TRUE(status_msg.empty()); + } + else + { + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), expected_status_msg); + } + } +} + +template +void HyperVHCSAPI_UnitTests::generic_operation_create_operation_fail(UutCallableT uut_callback, + PWSTR expected_status_msg) +{ + { // Verify that the dependencies are called with right data. + + EXPECT_CALL(mock_hcs_api, HcsOpenComputeSystem) + .WillOnce(DoAll( + [&](PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(id, L"test_vm"); + ASSERT_EQ(requestedAccess, GENERIC_ALL); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(nullptr))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + logger_scope.mock_logger->expect_log( + mpl::Level::error, + "perform_hcs_operation(...) > HcsCreateOperation failed!"); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = uut_callback(); + ASSERT_FALSE(status.success()); + + if (nullptr == expected_status_msg) + { + ASSERT_TRUE(status_msg.empty()); + } + else + { + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), expected_status_msg); + } + } +} + +template +void HyperVHCSAPI_UnitTests::generic_operation_fail(UutCallableT uut_callback, + PWSTR expected_status_msg) +{ + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsOpenComputeSystem) + .WillOnce(DoAll( + [&](PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(id, L"test_vm"); + ASSERT_EQ(requestedAccess, GENERIC_ALL); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + logger_scope.mock_logger->expect_log(mpl::Level::error, + "perform_hcs_operation(...) > Operation failed!"); + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = uut_callback(); + ASSERT_FALSE(status.success()); + if (nullptr == expected_status_msg) + { + ASSERT_TRUE(status_msg.empty()); + } + else + { + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), expected_status_msg); + } + } +} + +template +void HyperVHCSAPI_UnitTests::generic_operation_wait_for_operation_fail( + UutCallableT uut_callback, + PWSTR operation_result_document, + PWSTR expected_status_msg) +{ + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_hcs_api, HcsCreateOperation) + .WillOnce(DoAll( + [](const void* context, HCS_OPERATION_COMPLETION callback) { + ASSERT_EQ(nullptr, context); + ASSERT_EQ(nullptr, callback); + }, + Return(mock_operation_object))); + + EXPECT_CALL(mock_hcs_api, HcsCloseOperation).WillOnce([](HCS_OPERATION op) { + ASSERT_EQ(op, mock_operation_object); + }); + + EXPECT_CALL(mock_hcs_api, HcsWaitForOperationResult) + .WillOnce(DoAll( + [operation_result_document](HCS_OPERATION operation, + DWORD timeoutMs, + PWSTR* resultDocument) { + ASSERT_EQ(operation, mock_operation_object); + ASSERT_EQ(timeoutMs, 240000); + ASSERT_NE(nullptr, resultDocument); + ASSERT_EQ(nullptr, *resultDocument); + *resultDocument = operation_result_document; + }, + Return(E_POINTER))); + + EXPECT_CALL(mock_hcs_api, HcsOpenComputeSystem) + .WillOnce(DoAll( + [&](PCWSTR id, DWORD requestedAccess, HCS_SYSTEM* computeSystem) { + ASSERT_STREQ(id, L"test_vm"); + ASSERT_EQ(requestedAccess, GENERIC_ALL); + ASSERT_NE(nullptr, computeSystem); + ASSERT_EQ(nullptr, *computeSystem); + *computeSystem = mock_compute_system_object; + }, + Return(NOERROR))); + + EXPECT_CALL(mock_hcs_api, HcsCloseComputeSystem).WillOnce([](HCS_SYSTEM computeSystem) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + }); + + if (operation_result_document) + { + EXPECT_CALL(mock_hcs_api, LocalFree) + .WillOnce(DoAll([operation_result_document]( + HLOCAL ptr) { ASSERT_EQ(operation_result_document, ptr); }, + Return(nullptr))); + } + } + + { // Verify the expected outcome. + const auto& [status, status_msg] = uut_callback(); + ASSERT_FALSE(status.success()); + + if (nullptr == expected_status_msg) + { + ASSERT_TRUE(status_msg.empty()); + } + else + { + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), expected_status_msg); + } + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_happy_path) +{ + EXPECT_CALL(mock_hcs_api, HcsStartComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().start_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().start_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().start_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsStartComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().start_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, start_compute_system_wait_for_operation_result_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsStartComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().start_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_happy_path) +{ + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; + + EXPECT_CALL(mock_hcs_api, HcsShutDownComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().shutdown_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().shutdown_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().shutdown_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_fail) +{ + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; + + EXPECT_CALL(mock_hcs_api, HcsShutDownComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().shutdown_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, shutdown_compute_system_wait_for_operation_result_fail) +{ + static constexpr wchar_t expected_shutdown_option[] = LR"( + { + "Mechanism": "IntegrationService", + "Type": "Shutdown" + })"; + + EXPECT_CALL(mock_hcs_api, HcsShutDownComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_NE(options, nullptr); + ASSERT_STREQ(options, expected_shutdown_option); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().shutdown_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, terminate_compute_system_happy_path) +{ + EXPECT_CALL(mock_hcs_api, HcsTerminateComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().terminate_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, terminate_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().terminate_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, terminate_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().terminate_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, terminate_compute_system_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsTerminateComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().terminate_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, terminate_compute_system_wait_for_operation_result_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsTerminateComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().terminate_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, pause_compute_system_happy_path) +{ + static constexpr wchar_t expected_pause_option[] = LR"( + { + "SuspensionLevel": "Suspend", + "HostedNotification": { + "Reason": "Save" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsPauseComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(options); + const auto expected_options_no_whitespace = trim_whitespace(expected_pause_option); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().pause_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, pause_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().pause_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, pause_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().pause_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, pause_compute_system_fail) +{ + static constexpr wchar_t expected_pause_option[] = LR"( + { + "SuspensionLevel": "Suspend", + "HostedNotification": { + "Reason": "Save" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsPauseComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(options); + const auto expected_options_no_whitespace = trim_whitespace(expected_pause_option); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().pause_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, pause_compute_system_wait_for_operation_result_fail) +{ + static constexpr wchar_t expected_pause_option[] = LR"( + { + "SuspensionLevel": "Suspend", + "HostedNotification": { + "Reason": "Save" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsPauseComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR options) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(options); + const auto expected_options_no_whitespace = trim_whitespace(expected_pause_option); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().pause_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_happy_path) +{ + EXPECT_CALL(mock_hcs_api, HcsResumeComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().resume_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().resume_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().resume_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsResumeComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().resume_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resume_compute_system_wait_for_operation_result_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsResumeComputeSystem) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().resume_compute_system(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_happy_path) +{ + constexpr auto expected_modify_compute_system_configuration = LR"( + { + "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{288cc1ac-8f31-4a09-9e90-30ad0bcfdbca}", + "RequestType": "Add", + "Settings": { + "EndpointId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca", + "MacAddress": "00:00:00:00:00:00", + "InstanceId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsNetworkAdapter params{}; + params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; + params.mac_address = "00:00:00:00:00:00"; + + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return HCS().modify_compute_system(handle, add_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + HcsNetworkAdapter params{}; + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return HCS().modify_compute_system(handle, add_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsNetworkAdapter params{}; + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return HCS().modify_compute_system(handle, add_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_compute_system_fail) +{ + constexpr auto expected_modify_compute_system_configuration = LR"( + { + "ResourcePath": "VirtualMachine/Devices/NetworkAdapters/{288cc1ac-8f31-4a09-9e90-30ad0bcfdbca}", + "RequestType": "Add", + "Settings": { + "EndpointId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca", + "MacAddress": "00:00:00:00:00:00", + "InstanceId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsNetworkAdapter params{}; + params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; + params.mac_address = "00:00:00:00:00:00"; + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return HCS().modify_compute_system(handle, add_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, add_network_adapter_to_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": "Add", + "Settings": { + "EndpointId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca", + "MacAddress": "00:00:00:00:00:00", + "InstanceId": "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca" + } + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsNetworkAdapter params{}; + params.endpoint_guid = "288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"; + params.mac_address = "00:00:00:00:00:00"; + HcsRequest add_network_adapter_req{HcsResourcePath::NetworkAdapters(params.endpoint_guid), + HcsRequestType::Add(), + params}; + return HCS().modify_compute_system(handle, add_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +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", + "Settings": null + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return HCS().modify_compute_system(handle, remove_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return HCS().modify_compute_system(handle, remove_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, remove_network_adapter_from_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return HCS().modify_compute_system(handle, remove_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +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", + "Settings": null + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return HCS().modify_compute_system(handle, remove_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +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", + "Settings": null + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest remove_network_adapter_req{ + HcsResourcePath::NetworkAdapters("288cc1ac-8f31-4a09-9e90-30ad0bcfdbca"), + HcsRequestType::Remove()}; + return HCS().modify_compute_system(handle, remove_network_adapter_req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_happy_path) +{ + constexpr auto expected_modify_compute_system_configuration = LR"( + { + "ResourcePath": "VirtualMachine/ComputeTopology/Memory/SizeInMB", + "RequestType": "Update", + "Settings": 16384 + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest req{HcsResourcePath::Memory(), + HcsRequestType::Update(), + HcsModifyMemorySettings{16384}}; + + return HCS().modify_compute_system(handle, req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + HcsRequest req{HcsResourcePath::Memory(), + HcsRequestType::Update(), + HcsModifyMemorySettings{16384}}; + return HCS().modify_compute_system(handle, req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest req{HcsResourcePath::Memory(), + HcsRequestType::Update(), + HcsModifyMemorySettings{16384}}; + return HCS().modify_compute_system(handle, req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_fail) +{ + constexpr auto expected_modify_compute_system_configuration = LR"( + { + "ResourcePath": "VirtualMachine/ComputeTopology/Memory/SizeInMB", + "RequestType": "Update", + "Settings": 16384 + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest req{HcsResourcePath::Memory(), + HcsRequestType::Update(), + HcsModifyMemorySettings{16384}}; + return HCS().modify_compute_system(handle, req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, resize_memory_of_compute_system_wait_for_operation_result_fail) +{ + constexpr auto expected_modify_compute_system_configuration = LR"( + { + "ResourcePath": "VirtualMachine/ComputeTopology/Memory/SizeInMB", + "RequestType": "Update", + "Settings": 16384 + })"; + + EXPECT_CALL(mock_hcs_api, HcsModifyComputeSystem) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, + HCS_OPERATION operation, + PCWSTR configuration, + HANDLE identity) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(configuration); + const auto expected_options_no_whitespace = + trim_whitespace(expected_modify_compute_system_configuration); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + HcsRequest req{HcsResourcePath::Memory(), + HcsRequestType::Update(), + HcsModifyMemorySettings{16384}}; + return HCS().modify_compute_system(handle, req); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_happy_path) +{ + constexpr auto expected_vm_query = LR"( + { + "PropertyTypes":[] + })"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(propertyQuery); + const auto expected_options_no_whitespace = trim_whitespace(expected_vm_query); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_happy_path([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().get_compute_system_properties(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_hcs_open_fail) +{ + generic_operation_hcs_open_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + return HCS().get_compute_system_properties(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_create_operation_fail) +{ + generic_operation_create_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().get_compute_system_properties(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_fail) +{ + constexpr auto expected_vm_query = LR"( + { + "PropertyTypes":[] + })"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(propertyQuery); + const auto expected_options_no_whitespace = trim_whitespace(expected_vm_query); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(E_POINTER))); + + generic_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().get_compute_system_properties(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_properties_wait_for_operation_result_fail) +{ + constexpr auto expected_vm_query = LR"( + { + "PropertyTypes":[] + })"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + const auto options_no_whitespace = trim_whitespace(propertyQuery); + const auto expected_options_no_whitespace = trim_whitespace(expected_vm_query); + ASSERT_STREQ(options_no_whitespace.c_str(), expected_options_no_whitespace.c_str()); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail([&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + return HCS().get_compute_system_properties(handle); + }); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_happy_path) +{ + static wchar_t result_doc[21] = L"{\"State\": \"Running\"}"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_happy_path( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state]() { ASSERT_EQ(state, decltype(state)::running); }(); + return result; + }, + result_doc); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_no_state) +{ + static wchar_t result_doc[21] = L"{\"Frodo\": \"Baggins\"}"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](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); + }, + Return(NOERROR))); + + generic_operation_happy_path( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state]() { ASSERT_EQ(state, decltype(state)::stopped); }(); + return result; + }, + result_doc); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_hcs_open_fail) +{ + static wchar_t expected_status_msg[] = L"HcsCreateOperation failed!"; + + generic_operation_hcs_open_fail( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_FALSE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state, result]() { + ASSERT_EQ(state, decltype(state)::unknown); + ASSERT_EQ(static_cast(result.code), E_POINTER); + }(); + + return result; + }, + expected_status_msg); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_create_operation_fail) +{ + static wchar_t expected_status_msg[] = L"HcsCreateOperation failed!"; + generic_operation_create_operation_fail( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; + }, + expected_status_msg); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_fail) +{ + static wchar_t expected_status_msg[] = L"HCS operation failed!"; + + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, propertyQuery); + }, + Return(E_POINTER))); + + generic_operation_fail( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; + }, + expected_status_msg); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSAPI_UnitTests, get_compute_system_state_wait_for_operation_result_fail) +{ + EXPECT_CALL(mock_hcs_api, HcsGetComputeSystemProperties) + .WillOnce(DoAll( + [](HCS_SYSTEM computeSystem, HCS_OPERATION operation, PCWSTR propertyQuery) { + ASSERT_EQ(mock_compute_system_object, computeSystem); + ASSERT_EQ(mock_operation_object, operation); + ASSERT_EQ(nullptr, propertyQuery); + }, + Return(NOERROR))); + + generic_operation_wait_for_operation_fail( + [&]() { + HcsSystemHandle handle{nullptr}; + EXPECT_TRUE(HCS().open_compute_system("test_vm", handle)); + ComputeSystemState state{ComputeSystemState::unknown}; + const auto result = HCS().get_compute_system_state(handle, state); + [state]() { ASSERT_EQ(state, decltype(state)::unknown); }(); + return result; + }, + nullptr, + nullptr); +} + +} // namespace multipass::test diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_request.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_request.cpp new file mode 100644 index 00000000000..f0967d2878c --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_request.cpp @@ -0,0 +1,119 @@ +/* + * 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/unit/common.h" +#include "tests/unit/hyperv_api/hyperv_test_utils.h" + +#include +#include + +using multipass::hyperv::string_literal; +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 auto& expected) + { + const auto result = to_string(uut); + const std::basic_string v{expected}; + 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()}; + + static constexpr auto expected_result = string_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()}; + + static constexpr auto expected_result = string_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"; + uut.settings = settings; + static constexpr auto expected_result = string_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/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp new file mode 100644 index 00000000000..567c9e3a496 --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -0,0 +1,684 @@ +/* + * 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 "tests/unit/common.h" +#include "tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h" +#include "tests/unit/hyperv_api/mock_hyperv_hcs_wrapper.h" +#include "tests/unit/hyperv_api/mock_hyperv_virtdisk_wrapper.h" +#include "tests/unit/mock_file_ops.h" +#include "tests/unit/mock_status_monitor.h" +#include "tests/unit/stub_ssh_key_provider.h" +#include "tests/unit/stub_status_monitor.h" +#include "tests/unit/temp_dir.h" +#include "tests/unit/temp_file.h" + +namespace mp = multipass; +namespace mpt = multipass::test; +using namespace testing; + +namespace mhv = multipass::hyperv; +using uut_t = mhv::HCSVirtualMachine; +using hcs_handle_t = mhv::hcs::HcsSystemHandle; +using hcs_op_result_t = mhv::OperationResult; +using hcs_system_state_t = mhv::hcs::ComputeSystemState; + +struct PartiallyMockedHCSVM : public uut_t +{ + using uut_t::uut_t; + + MOCK_METHOD(std::string, ssh_exec, (const std::string& cmd, bool whisper), (override)); + MOCK_METHOD(void, drop_ssh_session, (), (override)); + MOCK_METHOD(void, + add_extra_interface_to_instance_cloud_init, + (const std::string&, const multipass::NetworkInterface&), + (const, override)); +}; + +using partially_mocked_uut_t = PartiallyMockedHCSVM; + +struct HyperVHCSVirtualMachine_UnitTests : public ::testing::Test +{ + mpt::TempFile dummy_image; + mpt::TempFile dummy_cloud_init_iso; + mpt::TempDir dummy_instances_dir; + const std::string dummy_vm_name{"lord-of-the-pings"}; + + mp::VirtualMachineDescription desc{2, + mp::MemorySize{"3M"}, + mp::MemorySize{}, // not used + dummy_vm_name, + "aa:bb:cc:dd:ee:ff", + {}, + "", + {dummy_image.name().toStdString(), "", "", "", {}, {}}, + dummy_cloud_init_iso.name(), + {}, + {}, + {}, + {}}; + + mpt::StubSSHKeyProvider stub_key_provider{}; + mpt::StubVMStatusMonitor stub_monitor{}; + + mpt::MockHCSWrapper::GuardedMock mock_hcs_wrapper_injection = + mpt::MockHCSWrapper::inject(); + mpt::MockHCSWrapper& mock_hcs = *mock_hcs_wrapper_injection.first; + + mpt::MockHCNWrapper::GuardedMock mock_hcn_wrapper_injection = + mpt::MockHCNWrapper::inject(); + mpt::MockHCNWrapper& mock_hcn = *mock_hcn_wrapper_injection.first; + + mpt::MockVirtDiskWrapper::GuardedMock mock_virtdisk_wrapper_injection = + mpt::MockVirtDiskWrapper::inject(); + mpt::MockVirtDiskWrapper& mock_virtdisk = *mock_virtdisk_wrapper_injection.first; + + inline static auto mock_handle_raw = reinterpret_cast(0xbadf00d); + hcs_handle_t mock_handle{mock_handle_raw, [](void*) {}}; + void* compute_system_callback_context{nullptr}; + void (*compute_system_callback)(HCS_EVENT* hcs_event, void* context){nullptr}; + + void default_open_success() + { + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const std::string& name, hcs_handle_t& out_handle) { + ASSERT_EQ(dummy_vm_name, name); + out_handle = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, set_compute_system_callback(Eq(mock_handle), _, _)) + .WillRepeatedly(DoAll( + [this](const hcs_handle_t& target_hcs_system, + void* context, + void (*callback)(HCS_EVENT* hcs_event, void* context)) { + compute_system_callback_context = context; + compute_system_callback = callback; + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([this](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + } + + void default_create_success() + { + // Open returns failure so the VM would be created + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillRepeatedly( + DoAll([this](const std::string& name, + hcs_handle_t& out_handle) { ASSERT_EQ(dummy_vm_name, name); }, + Return(hcs_op_result_t{HCS_E_SYSTEM_NOT_FOUND, L""}))); + + EXPECT_CALL(mock_hcs, set_compute_system_callback(Eq(mock_handle), _, _)) + .WillRepeatedly(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([this](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcn, delete_endpoint(EndsWith("aabbccddeeff"))) + .WillRepeatedly(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcn, create_endpoint(_)) + .WillRepeatedly(DoAll( + [this](const multipass::hyperv::hcn::CreateEndpointParameters& params) { + ASSERT_TRUE(params.mac_address.has_value()); + ASSERT_EQ(params.mac_address.value(), "aa-bb-cc-dd-ee-ff"); + ASSERT_EQ(params.network_guid, "abcd"); + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_virtdisk, + list_virtual_disk_chain(Eq(dummy_image.name().toStdString()), _, _)) + .WillRepeatedly( + DoAll([this](const std::filesystem::path& vhdx_path, + std::vector& chain, + std::optional max_depth) { chain.push_back(vhdx_path); }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, + grant_vm_access(Eq(dummy_vm_name), Eq(dummy_image.name().toStdString()))) + .WillRepeatedly(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL( + mock_hcs, + grant_vm_access(Eq(dummy_vm_name), Eq(dummy_instances_dir.path().toStdString()))) + .WillRepeatedly(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL( + mock_hcs, + grant_vm_access(Eq(dummy_vm_name), Eq(dummy_cloud_init_iso.name().toStdString()))) + .WillRepeatedly(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcs, create_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const multipass::hyperv::hcs::CreateComputeSystemParameters& params, + hcs_handle_t& out_hcs_system) { + ASSERT_EQ(params.memory_size_mb, 3); + ASSERT_EQ(params.name, dummy_vm_name); + ASSERT_EQ(params.network_adapters.size(), 1); + ASSERT_EQ(params.processor_count, 2); + ASSERT_EQ(params.scsi_devices.size(), 2); + ASSERT_EQ(params.shares.size(), 0); + out_hcs_system = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + } + + template + std::shared_ptr construct_vm(multipass::VMStatusMonitor* monitor = nullptr) + { + return std::make_shared("abcd", + desc, + monitor ? *monitor : stub_monitor, + stub_key_provider, + dummy_instances_dir.path()); + } +}; + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, construct_vm_class_exists_open) +{ + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillOnce(DoAll( + [this](const std::string& name, hcs_handle_t& out_handle) { + ASSERT_EQ(dummy_vm_name, name); + out_handle = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, set_compute_system_callback(Eq(mock_handle), _, _)) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .Times(1) + .WillRepeatedly( + DoAll([this](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, construct_vm_class_exists_open_error) +{ + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillOnce(DoAll([this](const std::string& name, + hcs_handle_t& out_handle) { ASSERT_EQ(dummy_vm_name, name); }, + Return(hcs_op_result_t{HCS_E_SYSTEM_NOT_CONFIGURED_FOR_OPERATION, L""}))); + std::shared_ptr uut{nullptr}; + ASSERT_THROW(uut = construct_vm(), multipass::hyperv::OpenComputeSystemException); + ASSERT_EQ(uut, nullptr); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, construct_vm_class_exists_create) +{ + default_create_success(); + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_start_success) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); + + uut->start(); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::starting); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_start_failure) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); + + EXPECT_THROW(uut->start(), mhv::StartComputeSystemException); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_start_resume_success) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::paused; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); + + uut->start(); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::starting); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_start_resume_failure) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::paused; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, resume_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); + + EXPECT_THROW(uut->start(), mhv::StartComputeSystemException); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_success) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, shutdown_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); + + uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Powerdown); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_powerdown_fail) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, shutdown_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); + + EXPECT_CALL(*uut, ssh_exec(Eq("sudo shutdown -h now"), _)).Times(1); + EXPECT_CALL(*uut, drop_ssh_session()).Times(1); + + uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Powerdown); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_halt) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); + + EXPECT_CALL(*uut, drop_ssh_session()).Times(1); + + uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Halt); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_success) +{ + auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, save_compute_system(Eq(mock_handle), _)) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(*mock_file_ops, exists(A())).WillOnce(Return(true)); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); + + uut->suspend(); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_failure) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::running; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); + + ASSERT_THROW(uut->suspend(), multipass::hyperv::SaveComputeSystemException); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_port) +{ + default_open_success(); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + EXPECT_EQ(uut->ssh_port(), multipass::default_ssh_port); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_ssh_hostname) +{ + default_open_success(); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + EXPECT_EQ(uut->ssh_hostname({}), uut->get_name() + ".mshome.net"); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, update_state) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce(DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::paused; }, + Return(hcs_op_result_t{0, L""}))); + + mpt::MockVMStatusMonitor mock_monitor{}; + EXPECT_CALL( + mock_monitor, + persist_state_for(Eq(dummy_vm_name), Eq(multipass::VirtualMachine::State::suspended))) + .Times(2); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm(&mock_monitor)); + + uut->handle_state_update(); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, update_cpus) +{ + default_create_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + uut->update_cpus(55); + + EXPECT_CALL(mock_hcs, create_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const multipass::hyperv::hcs::CreateComputeSystemParameters& params, + hcs_handle_t& out_hcs_system) { + ASSERT_EQ(params.memory_size_mb, 3); + ASSERT_EQ(params.name, dummy_vm_name); + ASSERT_EQ(params.network_adapters.size(), 1); + ASSERT_EQ(params.processor_count, 55); + ASSERT_EQ(params.scsi_devices.size(), 2); + ASSERT_EQ(params.shares.size(), 0); + out_hcs_system = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + uut->start(); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, resize_memory) +{ + default_create_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + uut->resize_memory(multipass::MemorySize::from_bytes((1024ll * 1024 * 1024) * 10)); + + EXPECT_CALL(mock_hcs, create_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const multipass::hyperv::hcs::CreateComputeSystemParameters& params, + hcs_handle_t& out_hcs_system) { + ASSERT_EQ(params.memory_size_mb, 10240); + ASSERT_EQ(params.name, dummy_vm_name); + ASSERT_EQ(params.network_adapters.size(), 1); + ASSERT_EQ(params.processor_count, 2); + ASSERT_EQ(params.scsi_devices.size(), 2); + ASSERT_EQ(params.shares.size(), 0); + out_hcs_system = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + uut->start(); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, resize_disk) +{ + default_create_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_virtdisk, resize_virtual_disk(Eq(desc.image.image_path), Eq(123456))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcs, start_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + uut->resize_disk(multipass::MemorySize::from_bytes(123456)); + + EXPECT_CALL(mock_hcs, create_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const multipass::hyperv::hcs::CreateComputeSystemParameters& params, + hcs_handle_t& out_hcs_system) { + ASSERT_EQ(params.memory_size_mb, 3); + ASSERT_EQ(params.name, dummy_vm_name); + ASSERT_EQ(params.network_adapters.size(), 1); + ASSERT_EQ(params.processor_count, 2); + ASSERT_EQ(params.scsi_devices.size(), 2); + ASSERT_EQ(params.shares.size(), 0); + out_hcs_system = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + uut->start(); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, add_network_interface) +{ + default_open_success(); + + multipass::NetworkInterface if_to_add; + if_to_add.mac_address = "ff:ee:dd:cc:bb:aa"; + if_to_add.id = "floaterface"; + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly( + DoAll([](const hcs_handle_t&, + hcs_system_state_t& state) { state = hcs_system_state_t::stopped; }, + Return(hcs_op_result_t{0, L""}))); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + std::string endpoint_guid{}; + + EXPECT_CALL(*uut, add_extra_interface_to_instance_cloud_init(_, _)).Times(1); + uut->add_network_interface(0, "ff:ee:dd:cc:bb:aa", if_to_add); +} diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine_factory.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine_factory.cpp new file mode 100644 index 00000000000..fb645f5183c --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine_factory.cpp @@ -0,0 +1,244 @@ +/* + * 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 "tests/unit/common.h" +#include "tests/unit/hyperv_api/mock_hyperv_hcn_wrapper.h" +#include "tests/unit/hyperv_api/mock_hyperv_hcs_wrapper.h" +#include "tests/unit/hyperv_api/mock_hyperv_virtdisk_wrapper.h" +#include "tests/unit/mock_platform.h" +#include "tests/unit/stub_ssh_key_provider.h" +#include "tests/unit/stub_status_monitor.h" +#include "tests/unit/temp_dir.h" + +#include + +namespace mp = multipass; +namespace mpt = multipass::test; +namespace mhv = multipass::hyperv; + +using hcs_handle_t = mhv::hcs::HcsSystemHandle; +using hcs_op_result_t = mhv::OperationResult; +using uut_t = mhv::HCSVirtualMachineFactory; +using namespace testing; + +struct HyperVHCSVirtualMachineFactory_UnitTests : public ::testing::Test +{ + mpt::TempDir dummy_data_dir; + mpt::StubSSHKeyProvider stub_key_provider{}; + mpt::StubVMStatusMonitor stub_monitor{}; + + mpt::MockHCSWrapper::GuardedMock mock_hcs_wrapper_injection = + mpt::MockHCSWrapper::inject(); + mpt::MockHCSWrapper& mock_hcs = *mock_hcs_wrapper_injection.first; + + mpt::MockHCNWrapper::GuardedMock mock_hcn_wrapper_injection = + mpt::MockHCNWrapper::inject(); + mpt::MockHCNWrapper& mock_hcn = *mock_hcn_wrapper_injection.first; + + mpt::MockVirtDiskWrapper::GuardedMock mock_virtdisk_wrapper_injection = + mpt::MockVirtDiskWrapper::inject(); + mpt::MockVirtDiskWrapper& mock_virtdisk = *mock_virtdisk_wrapper_injection.first; + + mpt::MockPlatform::GuardedMock attr{mpt::MockPlatform::inject()}; + mpt::MockPlatform* mock_platform = attr.first; + + inline static auto mock_handle_raw = reinterpret_cast(0xbadf00d); + hcs_handle_t mock_handle{mock_handle_raw, [](void*) {}}; + + auto construct_factory() + { + return std::make_shared(dummy_data_dir.path()); + } +}; + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, remove_resources_for_impl_vm_exists) +{ + auto vm_name = "test-vm"; + auto vm_guid = "this isn't a guid but this isn't a real implementation either"; + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillOnce(DoAll( + [&](const std::string& name, hcs_handle_t& out_handle) { + ASSERT_EQ(vm_name, name); + out_handle = mock_handle; + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, get_compute_system_guid(Eq(mock_handle), IsEmpty())) + .WillOnce(DoAll([&](const hcs_handle_t& target_hcs_system, + std::string& out_guid) { out_guid = vm_guid; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcn, enumerate_attached_endpoints(Eq(vm_guid), IsEmpty())) + .WillOnce(DoAll( + [&](const std::string& vm_guid, std::vector& endpoint_guids) { + endpoint_guids.emplace_back("this isn't an endpoint guid"); + endpoint_guids.emplace_back("this isn't either"); + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcn, delete_endpoint(Eq("this isn't an endpoint guid"))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + EXPECT_CALL(mock_hcn, delete_endpoint(Eq("this isn't either"))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_factory()); + uut->remove_resources_for(vm_name); +} + +TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, remove_resources_for_impl_does_not_exists) +{ + auto vm_name = "test-vm"; + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillOnce(DoAll( + [&](const std::string& name, hcs_handle_t& out_handle) { + ASSERT_EQ(vm_name, name); + out_handle = mock_handle; + }, + Return(hcs_op_result_t{1, L""}))); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_factory()); + uut->remove_resources_for(vm_name); +} + +TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, prepare_instance_image) +{ + std::shared_ptr uut{nullptr}; + + multipass::VMImage img; + img.image_path = "abcdef"; + multipass::VirtualMachineDescription desc; + desc.disk_space = multipass::MemorySize::from_bytes(123456); + + EXPECT_CALL(mock_virtdisk, + resize_virtual_disk(Eq(img.image_path), Eq(desc.disk_space.in_bytes()))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + ASSERT_NO_THROW(uut = construct_factory()); + uut->prepare_instance_image(img, desc); +} + +TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, prepare_instance_image_failed) +{ + std::shared_ptr uut{nullptr}; + + multipass::VMImage img; + img.image_path = "abcdef"; + multipass::VirtualMachineDescription desc; + desc.disk_space = multipass::MemorySize::from_bytes(123456); + + EXPECT_CALL(mock_virtdisk, + resize_virtual_disk(Eq(img.image_path), Eq(desc.disk_space.in_bytes()))) + .WillOnce(Return(hcs_op_result_t{1, L""})); + + ASSERT_NO_THROW(uut = construct_factory()); + EXPECT_THROW(uut->prepare_instance_image(img, desc), multipass::hyperv::ImageResizeException); +} + +TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, create_virtual_machine) +{ + std::shared_ptr uut{nullptr}; + multipass::VirtualMachineDescription desc; + multipass::NetworkInterfaceInfo interface1{.id = "aabb", .type = "Ethernet"}, + interface2{.id = "bbaa", .type = "Ethernet"}; + + desc.extra_interfaces = {{.id = interface1.id}, {.id = interface2.id}}; + + EXPECT_CALL(*mock_platform, get_network_interfaces_info()) + .WillRepeatedly(Return( + std::map{{interface1.id, interface1}, + {interface2.id, interface2}})); + + EXPECT_CALL(mock_hcn, enumerate_networks) + .WillOnce(DoAll( + [&](std::vector& network_guids) { + // vSwitch for one already exists, one will be created from scratch + network_guids.emplace_back("this isn't a network guid"); + // network_guids.emplace_back("this isn't either"); + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcn, query_network(Eq("this isn't a network guid"), _)) + .WillOnce(DoAll( + [&](const std::string&, mhv::hcn::HcnNetworkInfo& out) { + out.guid = "this isn't a network guid"; + out.name = fmt::format("Multipass vSwitch ({})", interface1.id); + out.type = "ICS"; + out.network_adapter_name = interface1.id; + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcn, create_network(_)) + // only expect call for bbaa. aabb's vSwitch already exists. + .WillOnce(DoAll( + [&](const multipass::hyperv::hcn::CreateNetworkParameters& params) { + constexpr auto expected_name = "Multipass vSwitch (bbaa)"; + EXPECT_EQ(params.name, expected_name); + EXPECT_EQ(params.type, mhv::hcn::HcnNetworkType::Transparent()); + EXPECT_EQ(params.guid, multipass::utils::make_uuid(expected_name).toStdString()); + ASSERT_EQ(params.policies.size(), 1); + EXPECT_EQ(params.policies[0].type, + mhv::hcn::HcnNetworkPolicyType::NetAdapterName()); + ASSERT_TRUE( + std::holds_alternative( + params.policies[0].settings)); + const auto& net_adapter_name = + std::get( + params.policies[0].settings); + EXPECT_EQ(net_adapter_name.net_adapter_name, interface2.id); + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, open_compute_system(_, _)) + .WillRepeatedly(DoAll( + [this](const std::string& name, hcs_handle_t& out_handle) { out_handle = mock_handle; }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, set_compute_system_callback(Eq(mock_handle), _, _)) + .WillRepeatedly(DoAll( + [this](const hcs_handle_t& target_hcs_system, + void* context, + void (*callback)(HCS_EVENT* hcs_event, void* context)) { + + }, + Return(hcs_op_result_t{0, L""}))); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly(DoAll( + [this](const hcs_handle_t&, mhv::hcs::ComputeSystemState& state) { + state = mhv::hcs::ComputeSystemState::running; + }, + Return(hcs_op_result_t{0, L""}))); + + ASSERT_NO_THROW(uut = construct_factory()); + + uut->prepare_networking(desc.extra_interfaces); + auto ptr = uut->create_virtual_machine(desc, stub_key_provider, stub_monitor); +} diff --git a/tests/unit/hyperv_api/test_ut_hyperv_virtdisk.cpp b/tests/unit/hyperv_api/test_ut_hyperv_virtdisk.cpp new file mode 100644 index 00000000000..ecf10136cb0 --- /dev/null +++ b/tests/unit/hyperv_api/test_ut_hyperv_virtdisk.cpp @@ -0,0 +1,724 @@ +/* + * 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/unit/hyperv_api/mock_virtdisk_api.h" +#include "tests/unit/mock_file_ops.h" +#include "tests/unit/mock_logger.h" + +#include +#include + +#include + +namespace mpt = multipass::test; +namespace mpl = multipass::logging; + +using namespace testing; + +namespace multipass::test +{ + +using hyperv::virtdisk::VirtDisk; + +struct HyperVVirtDisk_UnitTests : public ::testing::Test +{ + mpt::MockLogger::Scope logger_scope = [] { + auto v = mpt::MockLogger::inject(); + v.mock_logger->screen_logs(mpl::Level::info); + return v; + }(); + + mpt::MockVirtDiskAPI::GuardedMock mock_virtdisk_api_injection = + mpt::MockVirtDiskAPI::inject(); + mpt::MockVirtDiskAPI& mock_virtdisk_api = *mock_virtdisk_api_injection.first; + + // 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_virtdisk_api, OpenVirtualDisk).WillOnce(Return(ERROR_PATH_NOT_FOUND)); + 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) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, CreateVirtualDisk) + .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_NONE, Flags); + ASSERT_EQ(0, ProviderSpecificFlags); + ASSERT_NE(nullptr, Parameters); + ASSERT_EQ(Parameters->Version, CREATE_VIRTUAL_DISK_VERSION_2); + ASSERT_EQ(Parameters->Version2.MaximumSize, 2097152); + ASSERT_EQ(Parameters->Version2.BlockSizeInBytes, 1048576); + + ASSERT_EQ(nullptr, Overlapped); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + const hyperv::virtdisk::CreateVirtualDiskParameters params{.size_in_bytes = 2097152, + .path = "test.vhdx"}; + + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(params); + EXPECT_TRUE(status.success()); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_vhd_happy_path) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, CreateVirtualDisk) + .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.vhd"); + ASSERT_EQ(VirtualDiskAccessMask, VIRTUAL_DISK_ACCESS_NONE); + ASSERT_EQ(nullptr, SecurityDescriptor); + ASSERT_EQ(CREATE_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_EQ(0, ProviderSpecificFlags); + ASSERT_NE(nullptr, Parameters); + ASSERT_EQ(Parameters->Version, CREATE_VIRTUAL_DISK_VERSION_2); + ASSERT_EQ(Parameters->Version2.MaximumSize, 2097152); + ASSERT_EQ(Parameters->Version2.BlockSizeInBytes, 524288); + ASSERT_EQ(nullptr, Overlapped); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + const hyperv::virtdisk::CreateVirtualDiskParameters params{.size_in_bytes = 2097152, + .path = "test.vhd"}; + + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(params); + EXPECT_TRUE(status.success()); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +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)); + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, CreateVirtualDisk) + .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); + + ASSERT_EQ(nullptr, Overlapped); + ASSERT_NE(nullptr, Handle); + ASSERT_EQ(nullptr, *Handle); + + *Handle = mock_handle_object; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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_virtdisk_api, GetVirtualDiskInformation) + .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_virtdisk_api, CloseHandle(Eq(mock_handle_object))) + .Times(2) + .WillRepeatedly(Return(true)); + } + + const hyperv::virtdisk::CreateVirtualDiskParameters params{ + .size_in_bytes = 0, + .path = "test.vhdx", + .predecessor = hyperv::virtdisk::SourcePathParameters{"source.vhdx"}}; + + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(params); + EXPECT_TRUE(status.success()); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, create_virtual_disk_failed) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, CreateVirtualDisk) + .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 + + ) {}, + Return(ERROR_PATH_NOT_FOUND))); + logger_scope.mock_logger->expect_log( + mpl::Level::error, + "create_virtual_disk(...) > CreateVirtualDisk failed with 3!"); + } + + const hyperv::virtdisk::CreateVirtualDiskParameters params{.size_in_bytes = 2097152, + .path = "test.vhd"}; + + { + const auto& [status, status_msg] = VirtDisk().create_virtual_disk(params); + EXPECT_FALSE(status.success()); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"CreateVirtualDisk failed with 3!"); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_happy_path) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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"test.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))); + + EXPECT_CALL(mock_virtdisk_api, ResizeVirtualDisk) + .WillOnce(DoAll( + [](HANDLE VirtualDiskHandle, + RESIZE_VIRTUAL_DISK_FLAG Flags, + PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) { + ASSERT_EQ(mock_handle_object, VirtualDiskHandle); + ASSERT_EQ(RESIZE_VIRTUAL_DISK_FLAG_NONE, Flags); + ASSERT_NE(nullptr, Parameters); + ASSERT_EQ(Parameters->Version, RESIZE_VIRTUAL_DISK_VERSION_1); + ASSERT_EQ(Parameters->Version1.NewSize, 1234567); + ASSERT_EQ(nullptr, Overlapped); + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + { + const auto& [status, status_msg] = VirtDisk().resize_virtual_disk("test.vhdx", 1234567); + EXPECT_TRUE(status.success()); + EXPECT_TRUE(status_msg.empty()); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_open_failed) +{ + { // Verify that the dependencies are called with right data. + open_vhd_expect_failure(); + } + + { + const auto& [status, status_msg] = VirtDisk().resize_virtual_disk("test.vhdx", 1234567); + EXPECT_FALSE(status.success()); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"open_virtual_disk failed!"); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, resize_virtual_disk_resize_failed) +{ + + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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) { *Handle = mock_handle_object; }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, ResizeVirtualDisk) + .WillOnce(DoAll([](HANDLE VirtualDiskHandle, + RESIZE_VIRTUAL_DISK_FLAG Flags, + PRESIZE_VIRTUAL_DISK_PARAMETERS Parameters, + LPOVERLAPPED Overlapped) {}, + Return(ERROR_INVALID_PARAMETER))); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + logger_scope.mock_logger->expect_log( + mpl::Level::error, + "resize_virtual_disk(...) > ResizeVirtualDisk failed with 87!"); + } + + { + const auto& [status, status_msg] = VirtDisk().resize_virtual_disk("test.vhdx", 1234567); + EXPECT_FALSE(status.success()); + ASSERT_FALSE(status_msg.empty()); + ASSERT_STREQ(status_msg.c_str(), L"ResizeVirtualDisk failed with 87!"); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_happy_path) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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"test.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_virtdisk_api, GetVirtualDiskInformation) + .WillOnce(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); + ASSERT_EQ(GET_VIRTUAL_DISK_INFO_SIZE, VirtualDiskInfo->Version); + VirtualDiskInfo->Size.VirtualSize = 1111111; + VirtualDiskInfo->Size.BlockSize = 2222222; + VirtualDiskInfo->Size.PhysicalSize = 3333333; + VirtualDiskInfo->Size.SectorSize = 4444444; + }, + Return(ERROR_SUCCESS))) + .WillOnce(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); + ASSERT_EQ(GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE, VirtualDiskInfo->Version); + VirtualDiskInfo->VirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; + VirtualDiskInfo->VirtualStorageType.VendorId = + VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN; + }, + Return(ERROR_SUCCESS))); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + { + hyperv::virtdisk::VirtualDiskInfo info{}; + const auto& [status, status_msg] = VirtDisk().get_virtual_disk_info("test.vhdx", info); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + + ASSERT_TRUE(info.size.has_value()); + // ASSERT_TRUE(info.smallest_safe_virtual_size.has_value()); + ASSERT_TRUE(info.virtual_storage_type.has_value()); + + ASSERT_EQ(info.size->virtual_, 1111111); + ASSERT_EQ(info.size->block, 2222222); + ASSERT_EQ(info.size->physical, 3333333); + ASSERT_EQ(info.size->sector, 4444444); + + ASSERT_STREQ(info.virtual_storage_type.value().c_str(), "vhdx"); + // ASSERT_EQ(info.smallest_safe_virtual_size.value(), 123456); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, get_virtual_disk_info_fail_some) +{ + { // Verify that the dependencies are called with right data. + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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"test.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_virtdisk_api, GetVirtualDiskInformation) + .WillOnce(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); + ASSERT_EQ(GET_VIRTUAL_DISK_INFO_SIZE, VirtualDiskInfo->Version); + VirtualDiskInfo->Size.VirtualSize = 1111111; + VirtualDiskInfo->Size.BlockSize = 2222222; + VirtualDiskInfo->Size.PhysicalSize = 3333333; + VirtualDiskInfo->Size.SectorSize = 4444444; + }, + Return(ERROR_SUCCESS))) + .WillOnce(Return(ERROR_INVALID_PARAMETER)); + + EXPECT_CALL(mock_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + logger_scope.mock_logger->expect_log(mpl::Level::warning, + "get_virtual_disk_info(...) > failed to get 6"); + } + + { + hyperv::virtdisk::VirtualDiskInfo info{}; + const auto& [status, status_msg] = VirtDisk().get_virtual_disk_info("test.vhdx", info); + ASSERT_TRUE(status.success()); + ASSERT_TRUE(status_msg.empty()); + + ASSERT_TRUE(info.size.has_value()); + ASSERT_FALSE(info.virtual_storage_type.has_value()); + // ASSERT_TRUE(info.smallest_safe_virtual_size.has_value()); + + ASSERT_EQ(info.size->virtual_, 1111111); + ASSERT_EQ(info.size->block, 2222222); + ASSERT_EQ(info.size->physical, 3333333); + ASSERT_EQ(info.size->sector, 4444444); + + // ASSERT_EQ(info.smallest_safe_virtual_size.value(), 123456); + } +} + +// --------------------------------------------------------- + +TEST_F(HyperVVirtDisk_UnitTests, reparent_virtual_disk_happy_path) +{ + { // Verify that the dependencies are called with right data. + + EXPECT_CALL(mock_virtdisk_api, OpenVirtualDisk) + .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_virtdisk_api, SetVirtualDiskInformation) + .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_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + { + const auto& [status, status_msg] = + VirtDisk().reparent_virtual_disk("child.avhdx", "parent.vhdx"); + EXPECT_TRUE(status.success()); + 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(); + } + + { + const auto& [status, status_msg] = + VirtDisk().reparent_virtual_disk("child.avhdx", "parent.vhdx"); + EXPECT_FALSE(status.success()); + 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_virtdisk_api, OpenVirtualDisk) + .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_virtdisk_api, MergeVirtualDisk) + .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_virtdisk_api, CloseHandle(Eq(mock_handle_object))).WillOnce(Return(true)); + } + + { + const auto& [status, status_msg] = VirtDisk().merge_virtual_disk_into_parent("child.avhdx"); + EXPECT_TRUE(status.success()); + 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(); + } + + { + const auto& [status, status_msg] = VirtDisk().merge_virtual_disk_into_parent("child.avhdx"); + EXPECT_FALSE(status.success()); + EXPECT_FALSE(status_msg.empty()); + } +} + +} // namespace multipass::test diff --git a/tests/unit/mock_file_ops.h b/tests/unit/mock_file_ops.h index c2b67fb00cd..d41ae445302 100644 --- a/tests/unit/mock_file_ops.h +++ b/tests/unit/mock_file_ops.h @@ -118,8 +118,13 @@ class MockFileOps : public FileOps fs::copy_options, std::error_code&), (const, override)); + MOCK_METHOD(void, rename, (const fs::path& old_p, const fs::path& new_p), (override, const)); MOCK_METHOD(bool, exists, (const fs::path& path), (override, const)); - MOCK_METHOD(bool, exists, (const fs::path& path, std::error_code& err), (override, const)); + MOCK_METHOD(bool, + exists, + (const fs::path& path, std::error_code& err), + (override, const, noexcept)); + MOCK_METHOD(bool, is_symlink, (const fs::path& path), (override, const)); MOCK_METHOD(bool, is_directory, (const fs::path& path, std::error_code& err), @@ -132,7 +137,11 @@ class MockFileOps : public FileOps 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), diff --git a/tests/unit/mock_virtual_machine.h b/tests/unit/mock_virtual_machine.h index 30107dcff13..8f3c1c4edb4 100644 --- a/tests/unit/mock_virtual_machine.h +++ b/tests/unit/mock_virtual_machine.h @@ -101,7 +101,10 @@ 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, diff --git a/tests/unit/stub_virtual_machine.h b/tests/unit/stub_virtual_machine.h index 92cef552646..0d823a82c28 100644 --- a/tests/unit/stub_virtual_machine.h +++ b/tests/unit/stub_virtual_machine.h @@ -125,7 +125,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/unit/test_base_snapshot.cpp b/tests/unit/test_base_snapshot.cpp index 2e7a945d946..d3a74bcb35a 100644 --- a/tests/unit/test_base_snapshot.cpp +++ b/tests/unit/test_base_snapshot.cpp @@ -625,7 +625,8 @@ TEST_F(TestBaseSnapshot, eraseRemovesFile) snapshot.capture(); EXPECT_CALL(*mock_file_ops, - rename(Property(&QFile::fileName, Eq(expected_file_path)), Ne(expected_file_path))) + rename(MatcherCast(Property(&QFile::fileName, Eq(expected_file_path))), + Ne(expected_file_path))) .WillOnce(Return(true)); snapshot.erase(); @@ -643,7 +644,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)), _)) + 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))))) @@ -670,10 +672,12 @@ TEST_F(TestBaseSnapshot, restoresFileOnFailureToErase) snapshot.capture(); EXPECT_CALL(*mock_file_ops, - rename(Property(&QFile::fileName, Eq(expected_file_path)), Ne(expected_file_path))) + 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))); + 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/unit/test_base_virtual_machine.cpp b/tests/unit/test_base_virtual_machine.cpp index 6dd9a4f1cf8..9c21eb84035 100644 --- a/tests/unit/test_base_virtual_machine.cpp +++ b/tests/unit/test_base_virtual_machine.cpp @@ -486,17 +486,52 @@ TEST_F(BaseVM, providesSnapshotsView) vm.delete_snapshot(sname(i)); ASSERT_EQ(vm.get_num_snapshots(), 4); - auto snapshots = vm.view_snapshots(); - EXPECT_THAT(snapshots, SizeIs(4)); + { + // No predicate + auto snapshots = vm.view_snapshots({}); + + EXPECT_THAT(snapshots, SizeIs(4)); - std::vector snapshot_indices{}; - std::transform(snapshots.begin(), - snapshots.end(), - std::back_inserter(snapshot_indices), - [](const auto& snapshot) { return snapshot->get_index(); }); + std::vector snapshot_indices{}; + std::transform(snapshots.begin(), + snapshots.end(), + std::back_inserter(snapshot_indices), + [](const auto& snapshot) { return snapshot->get_index(); }); - EXPECT_THAT(snapshot_indices, UnorderedElementsAre(2, 5, 6, 8)); + EXPECT_THAT(snapshot_indices, UnorderedElementsAre(2, 5, 6, 8)); + } + + { + // Select nothing + auto snapshots = vm.view_snapshots([&](const auto& snapshot) { return false; }); + + EXPECT_THAT(snapshots, SizeIs(0)); + } + + { + // Select everything + auto snapshots = vm.view_snapshots([&](const auto& snapshot) { return true; }); + + EXPECT_THAT(snapshots, SizeIs(4)); + } + + { + // Select index 2 and 5 + auto snapshots = vm.view_snapshots([&](const multipass::Snapshot& snapshot) { + return snapshot.get_index() == 2 || snapshot.get_index() == 5; + }); + + EXPECT_THAT(snapshots, SizeIs(2)); + + std::vector snapshot_indices{}; + std::transform(snapshots.begin(), + snapshots.end(), + std::back_inserter(snapshot_indices), + [](const auto& snapshot) { return snapshot->get_index(); }); + + EXPECT_THAT(snapshot_indices, UnorderedElementsAre(2, 5)); + } } TEST_F(BaseVM, providesSnapshotsByIndex) diff --git a/tests/unit/test_data/cloud-init/README.md b/tests/unit/test_data/cloud-init/README.md new file mode 100644 index 00000000000..7e02d8f52bd --- /dev/null +++ b/tests/unit/test_data/cloud-init/README.md @@ -0,0 +1,3 @@ +Sample cloud-init iso for testing. + +To regenerate: `genisoimage -output cloud-init.iso -volid cidata -joliet -rock user-data meta-data` \ No newline at end of file diff --git a/tests/unit/test_data/cloud-init/cloud-init.iso b/tests/unit/test_data/cloud-init/cloud-init.iso new file mode 100644 index 00000000000..9debd2b037f Binary files /dev/null and b/tests/unit/test_data/cloud-init/cloud-init.iso differ diff --git a/tests/unit/test_data/cloud-init/meta-data b/tests/unit/test_data/cloud-init/meta-data new file mode 100644 index 00000000000..477ad36a50b --- /dev/null +++ b/tests/unit/test_data/cloud-init/meta-data @@ -0,0 +1,2 @@ +local-hostname: multipass-alpine-it +cloud-name: proof-of-concept diff --git a/tests/unit/test_data/cloud-init/user-data b/tests/unit/test_data/cloud-init/user-data new file mode 100644 index 00000000000..a0c2d9dcd52 --- /dev/null +++ b/tests/unit/test_data/cloud-init/user-data @@ -0,0 +1,6 @@ +#cloud-config +chpasswd: + list: | + root:password + alpine:password + expire: False diff --git a/tests/unit/test_data/cloud-vhdx/README.md b/tests/unit/test_data/cloud-vhdx/README.md new file mode 100644 index 00000000000..8c2d60d1a4b --- /dev/null +++ b/tests/unit/test_data/cloud-vhdx/README.md @@ -0,0 +1,9 @@ +This folder contains a lightweight virtual machine disk image (alpine linux) for the integration tests. + +The disk is split into 95MB chunks since GitHub's non-lfs file max size is 100MB. + +``` +split -b 95M alpine.vhdx alpine.vhdx.part- +``` + +Configuration: NoCloud-3.20.8-x86_64-UEFI-cloud-init-virtual \ No newline at end of file diff --git a/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-aa b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-aa new file mode 100644 index 00000000000..cb29be59172 Binary files /dev/null and b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-aa differ diff --git a/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ab b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ab new file mode 100644 index 00000000000..7e2cb25b57c Binary files /dev/null and b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ab differ diff --git a/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ac b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ac new file mode 100644 index 00000000000..45719d7a293 Binary files /dev/null and b/tests/unit/test_data/cloud-vhdx/alpine.vhdx.part-ac differ diff --git a/tests/unit/test_image_vault_utils.cpp b/tests/unit/test_image_vault_utils.cpp index 5dd7fbb6b9c..babc7abc17b 100644 --- a/tests/unit/test_image_vault_utils.cpp +++ b/tests/unit/test_image_vault_utils.cpp @@ -205,7 +205,7 @@ TEST_F(TestImageVaultUtils, extractFileWontDeleteFile) ++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/unit/test_sftpserver.cpp b/tests/unit/test_sftpserver.cpp index 7e9393deffe..b218618066d 100644 --- a/tests/unit/test_sftpserver.cpp +++ b/tests/unit/test_sftpserver.cpp @@ -1158,7 +1158,7 @@ TEST_F(SftpServer, renameCannotRemoveTargetFails) 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(); }); @@ -1205,7 +1205,7 @@ TEST_F(SftpServer, renameFailureFails) 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(); }); diff --git a/tests/unit/windows/test_platform_win.cpp b/tests/unit/windows/test_platform_win.cpp index 45662ebc359..9126787b466 100644 --- a/tests/unit/windows/test_platform_win.cpp +++ b/tests/unit/windows/test_platform_win.cpp @@ -146,7 +146,7 @@ TEST(PlatformWin, noExtraDaemonSettings) TEST(PlatformWin, testDefaultDriver) { - EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "virtualbox")); + EXPECT_THAT(MP_PLATFORM.default_driver(), AnyOf("hyperv", "hyperv_api", "virtualbox")); } TEST(PlatformWin, testDefaultPrivilegedMounts)