Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/platform/backends/hyperv_api/hcn/hyperv_hcn_ipam_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ struct HcnIpamType : FormatAsMixin<HcnIpamType>
return {"static"};
}

[[nodiscard]] bool operator==(const HcnIpamType& rhs) const
{
return value == rhs.value;
}

private:
HcnIpamType(std::string_view v) : value(v)
{
Expand Down
8 changes: 7 additions & 1 deletion src/platform/backends/hyperv_api/hcs_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,12 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state)
{
mpl::debug(get_name(), "set_state() -> HCS state `{}`", compute_system_state);

if (state == State::unavailable)
{
mpl::debug(get_name(), "set_state() -> Zone is unavailable");
return;
}

const auto prev_state = state;
switch (compute_system_state)
{
Expand All @@ -449,7 +455,7 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state)
if (state == prev_state)
return;

mpl::info(get_name(), "set_state() > State changed from {} to {}", prev_state, state);
mpl::info(get_name(), "set_state() -> State changed from {} to {}", prev_state, state);
}

void HCSVirtualMachine::start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ struct ResizeDiskException : public FormattedExceptionBase<>
using FormattedExceptionBase::FormattedExceptionBase;
};

struct CreateBridgeException : public FormattedExceptionBase<>
struct CreateNetworkException : public FormattedExceptionBase<>
{
using FormattedExceptionBase::FormattedExceptionBase;
};

struct CreateBridgeException : public CreateNetworkException
{
using CreateNetworkException::CreateNetworkException;
};

struct WindowsFeatureNotEnabledException : public FormattedExceptionBase<>
{
using FormattedExceptionBase::FormattedExceptionBase;
Expand Down
36 changes: 33 additions & 3 deletions src/platform/backends/hyperv_api/hcs_virtual_machine_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ 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
Expand All @@ -72,7 +71,8 @@ HCSVirtualMachineFactory::HCSVirtualMachineFactory(const Path& data_dir,
MP_UTILS.derive_instances_dir(data_dir,
HCSVirtualMachineFactory::get_backend_directory_name(),
instances_subdir),
az_manager)
az_manager),
az_network_guids{create_az_bridges(az_manager.get_zones())}
{
}

Expand All @@ -81,7 +81,7 @@ VirtualMachine::UPtr HCSVirtualMachineFactory::create_virtual_machine(
const SSHKeyProvider& key_provider,
VMStatusMonitor& monitor)
{
return std::make_unique<HCSVirtualMachine>(default_hyperv_switch_guid,
return std::make_unique<HCSVirtualMachine>(az_network_guids.at(desc.zone),
Comment thread
xmkg marked this conversation as resolved.
desc,
monitor,
key_provider,
Expand Down Expand Up @@ -343,4 +343,34 @@ void HCSVirtualMachineFactory::hypervisor_health_check()
}
}

std::unordered_map<std::string, std::string> HCSVirtualMachineFactory::create_az_bridges(
const AvailabilityZoneManager::Zones& zones)
{
std::unordered_map<std::string, std::string> az_mapping;
for (const auto& i : zones)
{
const auto& zone = i.get();
hcn::CreateNetworkParameters network_params{
.name = fmt::format("Multipass vNetwork ({})", zone.get_name()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default vSwitch had its DNS suffix set to .mshome.net, which allowed users to do this:

ssh test-vm.mshome.net

Also, this is how we discover the VMs assigned IP right now, and since the new AZ networks have no DNS suffix, we can't use it anymore, and VMs get stuck at start:

[2026-06-25T11:25:32.476] [debug] [HyperV-HCS-Wrapper] get_compute_system_state(...) > handle: (0x24db33f7eb0)
[2026-06-25T11:25:32.476] [trace] [HyperV-HCS-Wrapper] create_operation(...)
[2026-06-25T11:25:32.476] [debug] [HyperV-HCS-Wrapper] perform_hcs_operation(...) > result: true
[2026-06-25T11:25:32.476] [debug] [HyperV-HCS-Wrapper] wait_for_operation_result(...) > (0x24db32d8fd0), timeout: 240000 ms
[2026-06-25T11:25:32.476] [debug] [HyperV-HCS-Wrapper] wait_for_operation_result(...) > finished (0x24db32d8fd0), result_code: 0x0: The operation completed successfully.
[2026-06-25T11:25:32.476] [debug] [test-vm] set_state() -> HCS state `running`
[2026-06-25T11:25:32.476] [trace] [test-vm] ssh connection failed: 'Failed to resolve hostname test-vm.mshome.net

To support this, we need to add the DNS suffix parameter to the create_network call. It overlaps with the hostname resolution task I have. I'll update the HCN code in a separate PR to support this. It should be trivial -- I'll ping you once it's done.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we can use the HCN API to query the assigned IP address to the endpoint, but it would require more work. I'd say it would be a better choice -- something to consider in the future.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opted to implement querying the assigned IP from HCN instead. resolve_hostname approach is fragile since the hosts.ics file is plagued with a 25-year-old corruption bug.

#5097

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, <instance>.mshome.net works fine for me (see below). Maybe you already fixed it, or there's just some configuration difference between our systems? My test was just on a mostly-stock Windows 11 VM. (I had previously enabled HKML\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\IPEnableRouter because I think we need that for cross-zone communication, but pinging by hostname works for if I reset that value and restart.)

C:\Users\Jim\Documents\multipass\build>bin\multipass ls
Name                    State             IPv4             Image               Zone
bar                     Running           10.97.1.223      Ubuntu 26.04 LTS    zone2
foo                     Running           10.97.0.53       Ubuntu 26.04 LTS    zone1

C:\Users\Jim\Documents\multipass\build>ping foo.mshome.net

Pinging foo.mshome.net [10.97.0.53] with 32 bytes of data:
Reply from 10.97.0.53: bytes=32 time=1ms TTL=64
Reply from 10.97.0.53: bytes=32 time=1ms TTL=64
Reply from 10.97.0.53: bytes=32 time=1ms TTL=64
Reply from 10.97.0.53: bytes=32 time=1ms TTL=64

Ping statistics for 10.97.0.53:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 1ms, Maximum = 1ms, Average = 1ms

C:\Users\Jim\Documents\multipass\build>ping bar.mshome.net

Pinging bar.mshome.net [10.97.1.223] with 32 bytes of data:
Reply from 10.97.1.223: bytes=32 time=1ms TTL=64
Reply from 10.97.1.223: bytes=32 time=1ms TTL=64
Reply from 10.97.1.223: bytes=32 time=1ms TTL=64
Reply from 10.97.1.223: bytes=32 time=1ms TTL=64

Ping statistics for 10.97.1.223:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 1ms, Maximum = 1ms, Average = 1ms

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimporter Weird... I don't think it should be possible by default since the vSwitches we create don't have the mshome.net suffix on this branch -- I'll test the build on my 4 Windows VMs to confirm.

@xmkg xmkg Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimporter I've tested the branch -- the launches were successful everywhere except my dev-w11-home VM. I recall changing some registry settings during the hostname resolution experiments, which may explain why it fails on dev VM. Logs are below for completeness.
win11-hyperv-hcs-az-tests.txt

.type = hcn::HcnNetworkType::Ics(),
.flags = hcn::HcnNetworkFlags::enable_dhcp_server,
.guid = utils::make_uuid(network_params.name),
.ipams = {{.type = hcn::HcnIpamType::Static(),
.subnets = {{.ip_address_prefix = {zone.get_subnet().to_cidr()}}}}}};

const auto create_network_result = HCN().create_network(network_params);
if (!create_network_result &&
static_cast<HRESULT>(create_network_result.code) != HCN_E_NETWORK_ALREADY_EXISTS)
{
throw CreateNetworkException{"Could not create network for {}, status: {}",
zone.get_name(),
create_network_result};
}

az_mapping.emplace(zone.get_name(), network_params.guid);
}

return az_mapping;
}

} // namespace multipass::hyperv
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

#include <shared/base_virtual_machine_factory.h>

#include <unordered_map>

namespace multipass::hyperv
{

Expand Down Expand Up @@ -61,5 +63,10 @@ struct HCSVirtualMachineFactory final : public BaseVirtualMachineFactory
*/
[[nodiscard]] static std::vector<NetworkInterfaceInfo> get_adapters();
[[nodiscard]] static std::vector<NetworkInterfaceInfo> get_hyperv_vswitches();

[[nodiscard]] static std::unordered_map<std::string, std::string> create_az_bridges(
const AvailabilityZoneManager::Zones& zones);

std::unordered_map<std::string, std::string> az_network_guids;
};
} // namespace multipass::hyperv
27 changes: 27 additions & 0 deletions tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,33 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_success)

// ---------------------------------------------------------

TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_set_unavailable)
{
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_t> uut{nullptr};
ASSERT_NO_THROW(uut = construct_vm());

EXPECT_EQ(uut->state, multipass::VirtualMachine::State::running);

uut->set_available(false);

EXPECT_EQ(uut->state, multipass::VirtualMachine::State::unavailable);
}

// ---------------------------------------------------------

TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_failure)
{
default_open_success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ struct HyperVHCSVirtualMachineFactory_UnitTests : public ::testing::Test

auto construct_factory()
{
EXPECT_CALL(mock_hcn,
create_network(Field(&mhv::hcn::CreateNetworkParameters::name,
Eq("Multipass vNetwork (zone1)"))))
.WillOnce(DoAll(
[&](const mhv::hcn::CreateNetworkParameters& params) {
EXPECT_EQ(params.type, mhv::hcn::HcnNetworkType::Ics());
EXPECT_EQ(params.guid,
multipass::utils::make_uuid("Multipass vNetwork (zone1)"));
EXPECT_EQ(params.policies.size(), 0);
ASSERT_EQ(params.ipams.size(), 1);
EXPECT_EQ(params.ipams[0].type, mhv::hcn::HcnIpamType::Static());
ASSERT_EQ(params.ipams[0].subnets.size(), 1);
EXPECT_EQ(params.ipams[0].subnets[0].ip_address_prefix,
az_manager.get_zone("zone1").get_subnet().to_cidr());
},
Return(hcs_op_result_t{0, L""})));

return std::make_shared<uut_t>(dummy_data_dir.path(), az_manager);
}
};
Expand Down Expand Up @@ -161,13 +178,14 @@ TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, prepare_instance_image_failed)
.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);
EXPECT_THROW(uut->prepare_instance_image(img, desc), mhv::ImageResizeException);
}

TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, create_virtual_machine)
{
std::shared_ptr<uut_t> uut{nullptr};
multipass::VirtualMachineDescription desc;
desc.zone = "zone1";
multipass::NetworkInterfaceInfo interface1{.id = "aabb", .type = "Ethernet"},
interface2{.id = "bbaa", .type = "Ethernet"};

Expand Down Expand Up @@ -201,20 +219,18 @@ TEST_F(HyperVHCSVirtualMachineFactory_UnitTests, create_virtual_machine)
EXPECT_CALL(mock_hcn, create_network(_))
// only expect call for bbaa. aabb's vSwitch already exists.
.WillOnce(DoAll(
[&](const multipass::hyperv::hcn::CreateNetworkParameters& params) {
[&](const mhv::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));
ASSERT_EQ(params.policies.size(), 1);
EXPECT_EQ(params.policies[0].type,
mhv::hcn::HcnNetworkPolicyType::NetAdapterName());
ASSERT_TRUE(
std::holds_alternative<multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName>(
params.policies[0].settings));
const auto& net_adapter_name =
std::get<multipass::hyperv::hcn::HcnNetworkPolicyNetAdapterName>(
params.policies[0].settings);
ASSERT_TRUE(std::holds_alternative<mhv::hcn::HcnNetworkPolicyNetAdapterName>(
params.policies[0].settings));
const auto& net_adapter_name = std::get<mhv::hcn::HcnNetworkPolicyNetAdapterName>(
params.policies[0].settings);
EXPECT_EQ(net_adapter_name.net_adapter_name, interface2.id);
},
Return(hcs_op_result_t{0, L""})));
Expand Down
Loading