mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
Expose full interface address list and link state in cloud node API
Each host interface surfaced by the cloud node now reports: - ip_addresses: every IPv4 and IPv6 address (previously only a single IPv4 was collected internally and then dropped before the response) - status / speed / mtu / flags: operational state and link attributes sourced from psutil.net_if_stats(), with flags normalized to a list The legacy ip_address / netmask / mac_address fields are preserved so existing callers (compute link detection, GNS3 VM, VMware, has_netmask) keep working. The new fields travel through the existing interfaces payload that the controller forwards verbatim, so no controller-side change is required and the PUT / ports_mapping flow is unaffected.
This commit is contained in:
parent
a66740e154
commit
9721660cc3
@ -83,7 +83,16 @@ class Cloud(BaseNode):
|
||||
network_interfaces = gns3server.utils.interfaces.interfaces()
|
||||
for interface in network_interfaces:
|
||||
host_interfaces.append(
|
||||
{"name": interface["name"], "type": interface["type"], "special": interface["special"]}
|
||||
{
|
||||
"name": interface["name"],
|
||||
"type": interface["type"],
|
||||
"special": interface["special"],
|
||||
"ip_addresses": interface.get("ip_addresses", []),
|
||||
"status": interface.get("status", "down"),
|
||||
"speed": interface.get("speed", 0),
|
||||
"mtu": interface.get("mtu", 0),
|
||||
"flags": interface.get("flags", []),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@ -28,6 +28,22 @@ class HostInterfaceType(str, Enum):
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class IPAddressFamily(str, Enum):
|
||||
|
||||
ipv4 = "ipv4"
|
||||
ipv6 = "ipv6"
|
||||
|
||||
|
||||
class HostInterfaceIPAddress(BaseModel):
|
||||
"""
|
||||
An IP address (with optional netmask) bound to a host interface.
|
||||
"""
|
||||
|
||||
family: IPAddressFamily = Field(..., description="Address family (ipv4 or ipv6)")
|
||||
address: str = Field(..., description="IP address")
|
||||
netmask: Optional[str] = Field(None, description="Network mask, if available")
|
||||
|
||||
|
||||
class HostInterface(BaseModel):
|
||||
"""
|
||||
Interface on this host.
|
||||
@ -36,6 +52,13 @@ class HostInterface(BaseModel):
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
ip_addresses: List[HostInterfaceIPAddress] = Field(
|
||||
default_factory=list, description="All IPv4 and IPv6 addresses on this interface"
|
||||
)
|
||||
status: str = Field("down", description="Interface status (up or down)")
|
||||
speed: int = Field(0, description="Interface speed in Mbit/s (0 if unknown)")
|
||||
mtu: int = Field(0, description="Interface MTU")
|
||||
flags: List[str] = Field(default_factory=list, description="Interface flags")
|
||||
|
||||
|
||||
class EthernetType(str, Enum):
|
||||
|
||||
@ -198,6 +198,7 @@ def interfaces():
|
||||
results = []
|
||||
allowed_interfaces = Config.instance().settings.Server.allowed_interfaces
|
||||
net_if_addrs = psutil.net_if_addrs()
|
||||
net_if_stats = psutil.net_if_stats()
|
||||
for interface in sorted(net_if_addrs.keys()):
|
||||
if allowed_interfaces and interface not in allowed_interfaces and not interface.startswith("gns3tap"):
|
||||
log.warning(f"Interface '{interface}' is not allowed to be used on this server")
|
||||
@ -206,16 +207,47 @@ def interfaces():
|
||||
mac_address = ""
|
||||
netmask = ""
|
||||
interface_type = "ethernet"
|
||||
# Collect every IPv4 and IPv6 address on this interface. An interface may
|
||||
# carry several addresses of each family (or none at all), so we keep a
|
||||
# list in addition to the legacy single IPv4 value retained for backward
|
||||
# compatibility with existing callers (compute link detection, GNS3 VM,
|
||||
# VMware, has_netmask(), ...).
|
||||
ip_addresses = []
|
||||
for addr in net_if_addrs[interface]:
|
||||
# get the first available IPv4 address only
|
||||
if addr.family == socket.AF_INET:
|
||||
# legacy single-value behavior (keeps the last IPv4 seen)
|
||||
ip_address = addr.address
|
||||
netmask = addr.netmask
|
||||
ip_addresses.append(
|
||||
{"family": "ipv4", "address": addr.address, "netmask": addr.netmask or None}
|
||||
)
|
||||
elif addr.family == socket.AF_INET6:
|
||||
ip_addresses.append(
|
||||
{"family": "ipv6", "address": addr.address, "netmask": addr.netmask or None}
|
||||
)
|
||||
if addr.family == psutil.AF_LINK:
|
||||
mac_address = addr.address
|
||||
if interface.startswith("tap"):
|
||||
# found no way to reliably detect a TAP interface
|
||||
interface_type = "tap"
|
||||
# Operational state and link attributes (speed/mtu/flags) come from
|
||||
# psutil.net_if_stats(). An interface present in net_if_addrs is normally
|
||||
# also present here; fall back to neutral defaults when it is not.
|
||||
stats = net_if_stats.get(interface)
|
||||
if stats is not None:
|
||||
status = "up" if stats.isup else "down"
|
||||
speed = stats.speed
|
||||
mtu = stats.mtu
|
||||
# psutil returns flags either as a comma-separated string (>= 6.0) or
|
||||
# as a list (older versions); normalize to a list for a stable shape.
|
||||
flags = stats.flags
|
||||
if isinstance(flags, str):
|
||||
flags = [flag for flag in flags.split(",") if flag]
|
||||
else:
|
||||
status = "down"
|
||||
speed = 0
|
||||
mtu = 0
|
||||
flags = []
|
||||
results.append(
|
||||
{
|
||||
"id": interface,
|
||||
@ -224,6 +256,11 @@ def interfaces():
|
||||
"netmask": netmask,
|
||||
"mac_address": mac_address,
|
||||
"type": interface_type,
|
||||
"ip_addresses": ip_addresses,
|
||||
"status": status,
|
||||
"speed": speed,
|
||||
"mtu": mtu,
|
||||
"flags": flags,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -72,9 +72,9 @@ async def test_json_with_ports(on_gns3vm, compute_project, manager):
|
||||
}
|
||||
],
|
||||
"interfaces": [
|
||||
{'name': 'eth0', 'special': False, 'type': 'ethernet'},
|
||||
{'name': 'eth1', 'special': False, 'type': 'ethernet'},
|
||||
{'name': 'virbr0', 'special': True, 'type': 'ethernet'}
|
||||
{'name': 'eth0', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 1000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']},
|
||||
{'name': 'eth1', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'down', 'speed': 0, 'mtu': 1500, 'flags': ['broadcast']},
|
||||
{'name': 'virbr0', 'special': True, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 10000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']}
|
||||
]
|
||||
}
|
||||
|
||||
@ -111,9 +111,9 @@ async def test_json_without_ports(on_gns3vm, compute_project, manager):
|
||||
}
|
||||
],
|
||||
"interfaces": [
|
||||
{'name': 'eth0', 'special': False, 'type': 'ethernet'},
|
||||
{'name': 'eth1', 'special': False, 'type': 'ethernet'},
|
||||
{'name': 'virbr0', 'special': True, 'type': 'ethernet'}
|
||||
{'name': 'eth0', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 1000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']},
|
||||
{'name': 'eth1', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'down', 'speed': 0, 'mtu': 1500, 'flags': ['broadcast']},
|
||||
{'name': 'virbr0', 'special': True, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 10000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -325,9 +325,9 @@ def on_gns3vm(linux_platform):
|
||||
"""
|
||||
|
||||
with patch("gns3server.utils.interfaces.interfaces", return_value=[
|
||||
{"name": "eth0", "special": False, "type": "ethernet"},
|
||||
{"name": "eth1", "special": False, "type": "ethernet"},
|
||||
{"name": "virbr0", "special": True, "type": "ethernet"}]):
|
||||
{"name": "eth0", "special": False, "type": "ethernet", "ip_addresses": [], "status": "up", "speed": 1000, "mtu": 1500, "flags": ["up", "broadcast", "running", "multicast"]},
|
||||
{"name": "eth1", "special": False, "type": "ethernet", "ip_addresses": [], "status": "down", "speed": 0, "mtu": 1500, "flags": ["broadcast"]},
|
||||
{"name": "virbr0", "special": True, "type": "ethernet", "ip_addresses": [], "status": "up", "speed": 10000, "mtu": 1500, "flags": ["up", "broadcast", "running", "multicast"]}]):
|
||||
with patch("socket.gethostname", return_value="gns3vm"):
|
||||
yield
|
||||
|
||||
|
||||
@ -16,10 +16,21 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import collections
|
||||
from unittest.mock import patch
|
||||
|
||||
import psutil
|
||||
|
||||
from gns3server.utils.interfaces import interfaces, is_interface_up, has_netmask
|
||||
|
||||
|
||||
# psutil returns snicaddr namedtuples; mirror that shape for the mocks below.
|
||||
snicaddr = collections.namedtuple("snicaddr", ["family", "address", "netmask", "broadcast", "ptp"])
|
||||
# psutil.net_if_stats() returns snicstats namedtuples.
|
||||
snicstats = collections.namedtuple("snicstats", ["isup", "duplex", "speed", "mtu", "flags"])
|
||||
|
||||
|
||||
def test_interfaces():
|
||||
|
||||
# This test should pass on all platforms without crash
|
||||
@ -35,6 +46,111 @@ def test_interfaces():
|
||||
assert "mac_address" in interface
|
||||
assert "type" in interface
|
||||
assert "netmask" in interface
|
||||
assert "ip_addresses" in interface
|
||||
assert "status" in interface
|
||||
assert "speed" in interface
|
||||
assert "mtu" in interface
|
||||
assert "flags" in interface
|
||||
|
||||
|
||||
def _fake_net_if_addrs():
|
||||
# A representative set of host interfaces exercising the address-collection logic.
|
||||
return {
|
||||
# several IPv4 and several IPv6 addresses on the same interface
|
||||
"eth0": [
|
||||
snicaddr(socket.AF_INET, "192.168.1.5", "255.255.255.0", "192.168.1.255", None),
|
||||
snicaddr(socket.AF_INET, "10.0.0.5", "255.255.255.0", "10.0.0.255", None),
|
||||
snicaddr(socket.AF_INET6, "fe80::1", "ffff:ffff:ffff:ffff::", None, None),
|
||||
snicaddr(socket.AF_INET6, "2001:db8::1", "ffff:ffff:ffff:ffff::", None, None),
|
||||
snicaddr(psutil.AF_LINK, "00:11:22:33:44:55", None, None, None),
|
||||
],
|
||||
# no IP address at all (only a MAC)
|
||||
"eth1": [
|
||||
snicaddr(psutil.AF_LINK, "00:11:22:33:44:66", None, None, None),
|
||||
],
|
||||
# IPv6 only
|
||||
"eth2": [
|
||||
snicaddr(socket.AF_INET6, "2001:db8::2", "ffff:ffff:ffff:ffff::", None, None),
|
||||
snicaddr(psutil.AF_LINK, "00:11:22:33:44:77", None, None, None),
|
||||
],
|
||||
# present in net_if_addrs but absent from net_if_stats (exercises the fallback)
|
||||
"eth3": [
|
||||
snicaddr(psutil.AF_LINK, "00:11:22:33:44:88", None, None, None),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _fake_net_if_stats():
|
||||
# flags are returned as a comma-separated string by psutil >= 6.0
|
||||
return {
|
||||
"eth0": snicstats(True, 0, 1000, 1500, "up,broadcast,running,multicast"),
|
||||
"eth1": snicstats(False, 0, 0, 1500, "broadcast"),
|
||||
"eth2": snicstats(True, 0, 0, 1500, "up,running"),
|
||||
}
|
||||
|
||||
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_stats", _fake_net_if_stats)
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_addrs", _fake_net_if_addrs)
|
||||
def test_interfaces_collects_all_ip_addresses(config):
|
||||
|
||||
result = {iface["name"]: iface for iface in interfaces()}
|
||||
eth0 = result["eth0"]
|
||||
|
||||
# every IPv4 and IPv6 address is reported, preserving order
|
||||
assert eth0["ip_addresses"] == [
|
||||
{"family": "ipv4", "address": "192.168.1.5", "netmask": "255.255.255.0"},
|
||||
{"family": "ipv4", "address": "10.0.0.5", "netmask": "255.255.255.0"},
|
||||
{"family": "ipv6", "address": "fe80::1", "netmask": "ffff:ffff:ffff:ffff::"},
|
||||
{"family": "ipv6", "address": "2001:db8::1", "netmask": "ffff:ffff:ffff:ffff::"},
|
||||
]
|
||||
# legacy single-IPv4 fields are kept for backward compatibility
|
||||
assert eth0["ip_address"] == "10.0.0.5"
|
||||
assert eth0["netmask"] == "255.255.255.0"
|
||||
# link attributes and up status come from net_if_stats (flags split into a list)
|
||||
assert eth0["status"] == "up"
|
||||
assert eth0["speed"] == 1000
|
||||
assert eth0["mtu"] == 1500
|
||||
assert eth0["flags"] == ["up", "broadcast", "running", "multicast"]
|
||||
|
||||
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_stats", _fake_net_if_stats)
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_addrs", _fake_net_if_addrs)
|
||||
def test_interfaces_with_no_address(config):
|
||||
|
||||
result = {iface["name"]: iface for iface in interfaces()}
|
||||
eth1 = result["eth1"]
|
||||
assert eth1["ip_addresses"] == []
|
||||
assert eth1["ip_address"] == ""
|
||||
assert eth1["netmask"] == ""
|
||||
assert eth1["status"] == "down"
|
||||
assert eth1["flags"] == ["broadcast"]
|
||||
|
||||
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_stats", _fake_net_if_stats)
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_addrs", _fake_net_if_addrs)
|
||||
def test_interfaces_with_ipv6_only(config):
|
||||
|
||||
result = {iface["name"]: iface for iface in interfaces()}
|
||||
eth2 = result["eth2"]
|
||||
assert eth2["ip_addresses"] == [
|
||||
{"family": "ipv6", "address": "2001:db8::2", "netmask": "ffff:ffff:ffff:ffff::"},
|
||||
]
|
||||
assert eth2["ip_address"] == ""
|
||||
assert eth2["status"] == "up"
|
||||
assert eth2["mtu"] == 1500
|
||||
|
||||
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_stats", _fake_net_if_stats)
|
||||
@patch("gns3server.utils.interfaces.psutil.net_if_addrs", _fake_net_if_addrs)
|
||||
def test_interfaces_without_stats_entry(config):
|
||||
# an interface present in net_if_addrs but missing from net_if_stats falls
|
||||
# back to neutral defaults instead of crashing
|
||||
result = {iface["name"]: iface for iface in interfaces()}
|
||||
eth3 = result["eth3"]
|
||||
assert eth3["status"] == "down"
|
||||
assert eth3["speed"] == 0
|
||||
assert eth3["mtu"] == 0
|
||||
assert eth3["flags"] == []
|
||||
|
||||
|
||||
def test_has_netmask(config):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user