diff --git a/core/src/main/java/com/cloud/agent/api/GetVmIpAddressAnswer.java b/core/src/main/java/com/cloud/agent/api/GetVmIpAddressAnswer.java new file mode 100644 index 000000000000..40c05f3e98ce --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVmIpAddressAnswer.java @@ -0,0 +1,40 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package com.cloud.agent.api; + +/** + * Answer to {@link GetVmIpAddressCommand}. The IPv4 address is carried in {@link #getDetails()} for + * compatibility with agents answering with a plain {@link Answer}; the (global) IPv6 address, if any, is optional. + */ +public class GetVmIpAddressAnswer extends Answer { + + private String ip6Address; + + public GetVmIpAddressAnswer() { + } + + public GetVmIpAddressAnswer(GetVmIpAddressCommand command, boolean result, String ip4Address, String ip6Address) { + super(command, result, ip4Address); + this.ip6Address = ip6Address; + } + + public String getIp6Address() { + return ip6Address; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java index c7fde1414101..591a0f2756fc 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java @@ -23,6 +23,7 @@ import java.util.List; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVmIpAddressAnswer; import com.cloud.agent.api.GetVmIpAddressCommand; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.resource.CommandWrapper; @@ -58,10 +59,11 @@ static void init() { @Override public Answer execute(final GetVmIpAddressCommand command, final LibvirtComputingResource libvirtComputingResource) { String ip = null; + String ip6 = null; boolean result = false; String vmName = command.getVmName(); if (!NetUtils.verifyDomainNameLabel(vmName, true)) { - return new Answer(command, result, ip); + return new GetVmIpAddressAnswer(command, result, ip, ip6); } String sanitizedVmName = sanitizeBashCommandArgument(vmName); @@ -70,7 +72,9 @@ public Answer execute(final GetVmIpAddressCommand command, final LibvirtComputin init(); - ip = ipFromDomIf(sanitizedVmName, networkCidr, macAddress); + Pair ipAddresses = ipFromDomIf(sanitizedVmName, networkCidr, macAddress); + ip = ipAddresses.first(); + ip6 = ipAddresses.second(); if (ip == null && networkCidr != null) { if(!command.isWindows()) { @@ -80,18 +84,21 @@ public Answer execute(final GetVmIpAddressCommand command, final LibvirtComputin } } - if(ip != null){ + // success needs an IPv4 address or a global IPv6 address (IPv6-only guests); a link-local IPv6 address alone + // is only reported along with an IPv4 address + if (ip != null || (ip6 != null && !NetUtils.isLinkLocalIp6(ip6))) { result = true; - logger.debug("GetVmIp: "+ vmName + " Found Ip: "+ip); + logger.debug("GetVmIp: " + vmName + " Found Ip: " + ip + ", Ipv6: " + ip6); } else { logger.warn("GetVmIp: "+ vmName + " IP not found."); } - return new Answer(command, result, ip); + return new GetVmIpAddressAnswer(command, result, ip, ip6); } - private String ipFromDomIf(String sanitizedVmName, String networkCidr, String macAddress) { + private Pair ipFromDomIf(String sanitizedVmName, String networkCidr, String macAddress) { String ip = null; + String ip6 = null; List commands = new ArrayList<>(); commands.add(new String[]{virsh_path, "domifaddr", sanitizedVmName, "--source", "agent"}); Pair response = executePipedCommands(commands, 0); @@ -102,15 +109,17 @@ private String ipFromDomIf(String sanitizedVmName, String networkCidr, String ma if (networkCidr == null || NetUtils.isIpWithInCidrRange(ipv4, networkCidr)) { ip = ipv4; } + ip6 = ipAddresses.second(); } else { logger.error("ipFromDomIf: Command execution failed for VM: " + sanitizedVmName); } - return ip; + return new Pair<>(ip, ip6); } private Pair getIpAddresses(String output, String macAddress) { String ipv4 = null; String ipv6 = null; + String linkLocalIpv6 = null; boolean found = false; String[] lines = output.split("\n"); for (String line : lines) { @@ -130,12 +139,29 @@ private Pair getIpAddresses(String output, String macAddress) { found = true; String ipFamily = parts[parts.length - 2]; String ipPart = parts[parts.length - 1].split("/")[0]; + // Skip link-local IPv4 addresses (e.g. Windows APIPA 169.254.0.0/16 before a DHCP lease is obtained), + // they are not routable and must not be reported as the instance IP if (ipFamily.equals("ipv4")) { - ipv4 = ipPart; + if (NetUtils.isLinkLocalIp4(ipPart)) { + logger.debug(String.format("Ignoring link-local ipv4 %s with mac address %s", ipPart, macAddress)); + } else { + ipv4 = ipPart; + } } else if (ipFamily.equals("ipv6")) { - ipv6 = ipPart; + // keep the first global address, and the first link-local one as a fallback + if (NetUtils.isLinkLocalIp6(ipPart)) { + if (linkLocalIpv6 == null) { + int zoneIndex = ipPart.indexOf('%'); + linkLocalIpv6 = zoneIndex >= 0 ? ipPart.substring(0, zoneIndex) : ipPart; + } + } else if (ipv6 == null) { + ipv6 = ipPart; + } } } + if (ipv6 == null) { + ipv6 = linkLocalIpv6; + } logger.debug(String.format("Found ipv4: %s and ipv6: %s with mac address %s", ipv4, ipv6, macAddress)); return new Pair<>(ipv4, ipv6); } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java index f104eadd18be..acc78e4c4c72 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java @@ -35,6 +35,7 @@ import org.mockito.MockitoAnnotations; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVmIpAddressAnswer; import com.cloud.agent.api.GetVmIpAddressCommand; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.utils.Pair; @@ -58,6 +59,24 @@ public class LibvirtGetVmIpAddressCommandWrapperTest { " Loopback Pseudo-Interface 1 ipv6 ::1/128\n" + // " - - ipv4 127.0.0.1/8\n"; + // Windows guest that has not obtained a DHCP lease yet and self-assigned an APIPA address + private static String VIRSH_DOMIF_OUTPUT_WINDOWS_APIPA = " Name MAC address Protocol Address\n" + // + "-------------------------------------------------------------------------------\n" + // + " Ethernet 02:01:00:d0:00:87 ipv6 fe80::df84:2984:e396:8ac3%6/64\n" + // + " - - ipv4 169.254.149.120/16\n" + // + " Loopback Pseudo-Interface 1 ipv6 ::1/128\n" + // + " - - ipv4 127.0.0.1/8\n"; + + // Same Windows guest once the DHCP lease has been obtained + private static String VIRSH_DOMIF_OUTPUT_WINDOWS_DHCP = " Name MAC address Protocol Address\n" + // + "-------------------------------------------------------------------------------\n" + // + " Ethernet 02:01:00:d0:00:87 ipv6 fd00:ac19:0:1910:0:d3fc:4177:2c32/128\n" + // + " - - ipv6 fe80::df84:2984:e396:8ac3%6/64\n" + // + " - - ipv4 172.25.16.236/22\n" + // + " - - ipv4 169.254.149.120/16\n" + // + " Loopback Pseudo-Interface 1 ipv6 ::1/128\n" + // + " - - ipv4 127.0.0.1/8\n"; + @Before public void setUp() { MockitoAnnotations.openMocks(this); @@ -324,6 +343,116 @@ public void testIpFromWindowsRegistryCommandExecutionFailure() { } } + private Answer executeOnL2Network(String domIfOutput, boolean windows) { + LibvirtComputingResource libvirtComputingResource = mock(LibvirtComputingResource.class); + GetVmIpAddressCommand getVmIpAddressCommand = mock(GetVmIpAddressCommand.class); + LibvirtGetVmIpAddressCommandWrapper commandWrapper = new LibvirtGetVmIpAddressCommandWrapper(); + try (MockedStatic