blob: 485d52d79e87c5cd19fac794be6ebf794dc2e977 [file]
#!/usr/bin/env python3
#
# Copyright 2025 The Fuchsia Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import ipaddress
import time
from enum import StrEnum
from libs.ssh.connection import SshConnection
# Default max seconds it takes to Duplicate Address Detection to finish before
# assigning an IPv6 address.
DAD_TIMEOUT_SEC = 30
class AddrType(StrEnum):
ipv4_private = "ipv4_private"
"""Any 192.168, 172.16, 10, or 169.254 addresses"""
ipv4_public = "ipv4_public"
"""Any IPv4 public addresses"""
ipv6_link_local = "ipv6_link_local"
"""Any fe80:: addresses"""
ipv6_private_local = "ipv6_private_local"
"""Any fd00:: addresses"""
ipv6_public = "ipv6_public"
"""Any publicly routable addresses"""
def get_interface_ip_addresses(
comm_channel: SshConnection,
interface: str,
) -> dict[AddrType, list[str]]:
"""Gets all of the ip addresses, ipv4 and ipv6, associated with a
particular interface name.
Args:
comm_channel: How to send commands to a device. Must be an SshConnection.
interface: The interface name on the device, ie eth0
Returns:
A dictionary of lists of the various IP addresses:
ipv4_private: Any 192.168, 172.16, 10, or 169.254 addresses
ipv4_public: Any IPv4 public addresses
ipv6_link_local: Any fe80:: addresses
ipv6_private_local: Any fd00:: addresses
ipv6_public: Any publicly routable addresses
"""
ip = comm_channel.run(["ip", "-o", "addr", "show", interface])
addrs = [
addr.replace("/", " ").split()[3]
for addr in ip.stdout.decode("utf-8").splitlines()
if len(addr.split()) > 3
]
ipv4_private_addresses = []
ipv4_public_addresses = []
ipv6_link_local_addresses = []
ipv6_private_local_addresses = []
ipv6_public_addresses = []
for addr in addrs:
on_device_ip = ipaddress.ip_address(addr)
if on_device_ip.version == 4:
if on_device_ip.is_private:
ipv4_private_addresses.append(str(on_device_ip))
elif on_device_ip.is_global or (
# Carrier private doesn't have a property, so we check if
# all other values are left unset.
not on_device_ip.is_reserved
and not on_device_ip.is_unspecified
and not on_device_ip.is_link_local
and not on_device_ip.is_loopback
and not on_device_ip.is_multicast
):
ipv4_public_addresses.append(str(on_device_ip))
elif on_device_ip.version == 6:
if on_device_ip.is_link_local:
ipv6_link_local_addresses.append(str(on_device_ip))
elif on_device_ip.is_private:
ipv6_private_local_addresses.append(str(on_device_ip))
elif on_device_ip.is_global:
ipv6_public_addresses.append(str(on_device_ip))
return {
AddrType.ipv4_private: ipv4_private_addresses,
AddrType.ipv4_public: ipv4_public_addresses,
AddrType.ipv6_link_local: ipv6_link_local_addresses,
AddrType.ipv6_private_local: ipv6_private_local_addresses,
AddrType.ipv6_public: ipv6_public_addresses,
}
class AddressTimeout(TimeoutError):
pass
class MultipleAddresses(ValueError):
pass
def get_addr(
comm_channel: SshConnection,
interface: str,
addr_type: AddrType = AddrType.ipv4_private,
timeout_sec: int | None = None,
) -> str:
"""Get the requested type of IP address for an interface; if an address is
not available, retry until the timeout has been reached.
Args:
comm_channel: How to send commands to a device.
interface: The interface name on the device, ie eth0.
addr_type: Type of address to get as defined by the return value of
get_interface_ip_addresses.
timeout_sec: Seconds to wait to acquire an address if there isn't one
already available. If fetching an IPv4 address, the default is 3
seconds. If IPv6, the default is 30 seconds for Duplicate Address
Detection.
Returns:
A string containing the requested address.
Raises:
ValueError: Invalid addr_type
AddressTimeout: No address is available after timeout_sec
MultipleAddresses: Several addresses are available
"""
if not timeout_sec:
if "ipv4" in addr_type:
timeout_sec = 3
elif "ipv6" in addr_type:
timeout_sec = DAD_TIMEOUT_SEC
else:
raise ValueError(f'Unknown addr_type "{addr_type}"')
timeout = time.time() + timeout_sec
while time.time() < timeout:
addrs_dict = get_interface_ip_addresses(comm_channel, interface)
if addr_type not in addrs_dict:
raise ValueError(f'Unknown addr_type "{addr_type}"')
ip_addrs = addrs_dict[addr_type]
if len(ip_addrs) > 1:
raise MultipleAddresses(
f'Expected only one "{addr_type}" address, got {ip_addrs}'
)
elif len(ip_addrs) == 1:
return ip_addrs[0]
raise AddressTimeout(
f'No available "{addr_type}" address after {timeout_sec}s'
)