Skip to content

Commit

Permalink
cli: add container support to show interfaces
Browse files Browse the repository at this point in the history
Make show interfaces container aware. This is done by looking though
all podman containers for interfaces and looking up there info from
network namespaces on the main system / hypervisor.

Interfaces controlled by containers are clearly marked in the "show
interfaces" output, with a single gray row. Telling the user that they
belong to one or more named containers. The user can then run "show
interface name NAME" on interfaces owned by containers, which provided
some additional info, such as mac address and stat counters.

The patch also add support for printing veth peers which are owned by
containers.

Lastly, the patch also adds test cases for this functionality.

Signed-off-by: Richard Alpe <richard@bit42.se>
  • Loading branch information
rical committed Oct 29, 2024
1 parent 31061fc commit b9492c7
Show file tree
Hide file tree
Showing 14 changed files with 762 additions and 7 deletions.
10 changes: 9 additions & 1 deletion src/confd/yang/infix-if-container.yang
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ submodule infix-if-container {
Ensures a container interface can never be a bridge port, or
LAG member, at the same time.";

revision 2024-10-29 {
description "Add read only container list to container-network";
reference "internal";
}
revision 2024-01-15 {
description "Initial revision.";
reference "internal";
Expand Down Expand Up @@ -64,7 +68,11 @@ submodule infix-if-container {
base container-network;
}
}

leaf-list containers {
type string;
config false;
description "List of containers using this interface";
}
list subnet {
description "Static IP ranges to hand out addresses to containers from.
Expand Down
2 changes: 1 addition & 1 deletion src/klish-plugin-infix/xml/infix.xml
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@
<ACTION sym="script">
if [ -n "$KLISH_PARAM_name" ]; then
sysrepocfg -f json -X -d operational -x \
"/ietf-interfaces:interfaces/interface[name='$KLISH_PARAM_name']" | \
"/ietf-interfaces:interfaces/interface[name=\"$KLISH_PARAM_name\"]" | \
/usr/libexec/statd/cli-pretty "show-interfaces" -n "$KLISH_PARAM_name"
else
sysrepocfg -f json -X -d operational -m ietf-interfaces | \
Expand Down
24 changes: 24 additions & 0 deletions src/statd/python/cli_pretty/cli_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ def yellow(txt):
def underline(txt):
return Decore.decorate("4", txt, "24")

@staticmethod
def gray_bg(txt):
return Decore.decorate("100", txt)

def datetime_now():
if UNIT_TEST:
return datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
Expand Down Expand Up @@ -273,6 +277,7 @@ def __init__(self, data):
self.bridge = get_json_data('', self.data, 'infix-interfaces:bridge-port', 'bridge')
self.pvid = get_json_data('', self.data, 'infix-interfaces:bridge-port', 'pvid')
self.stp_state = get_json_data('', self.data, 'infix-interfaces:bridge-port', 'stp-state')
self.containers = get_json_data('', self.data, 'infix-interfaces:container-network', 'containers')

if data.get('statistics'):
self.in_octets = data.get('statistics').get('in-octets', '')
Expand Down Expand Up @@ -302,6 +307,10 @@ def __init__(self, data):
def is_vlan(self):
return self.type == "infix-if-type:vlan"

def is_in_container(self):
# Return negative if cointainer isn't set or is an empty list
return getattr(self, 'containers', None)

def is_bridge(self):
return self.type == "infix-if-type:bridge"

Expand Down Expand Up @@ -436,7 +445,18 @@ def pr_vlan(self, _ifaces):
parent.pr_name(pipe='└ ')
parent.pr_proto_eth()

def pr_container(self):
row = f"{self.name:<{Pad.iface}}"
row += f"{'container':<{Pad.proto}}"
row += f"{'':<{Pad.state}}"
row += f"{', ' . join(self.containers):<{Pad.data}}"

print(Decore.gray_bg(row))

def pr_iface(self):
if self.is_in_container():
print(Decore.gray_bg(f"{'owned by container':<{20}}: {', ' . join(self.containers)}"))

print(f"{'name':<{20}}: {self.name}")
print(f"{'index':<{20}}: {self.index}")
if self.mtu:
Expand Down Expand Up @@ -564,6 +584,10 @@ def pr_interface_list(json):
if iface.name == "lo":
continue

if iface.is_in_container():
iface.pr_container()
continue

if iface.is_bridge():
iface.pr_bridge(ifaces)
continue
Expand Down
85 changes: 80 additions & 5 deletions src/statd/python/yanger/yanger.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,18 +629,52 @@ def get_brport_multicast(ifname):
def get_ip_link():
"""Fetch interface link information from kernel"""
return run_json_cmd(['ip', '-s', '-d', '-j', 'link', 'show'],
f"ip-link-show.json")
"ip-link-show.json")

def netns_get_ip_link(netns):
"""Fetch interface link information from within a network namespace"""
return run_json_cmd(['ip', 'netns', 'exec', netns, 'ip', '-s', '-d', '-j', 'link', 'show'],
f"netns-{netns}-ip-link-show.json")

def get_ip_addr():
"""Fetch interface address information from kernel"""
return run_json_cmd(['ip', '-j', 'addr', 'show'],
f"ip-addr-show.json")
"ip-addr-show.json")

def netns_get_ip_addr(netns):
"""Fetch interface address information from within a network namespace"""
return run_json_cmd(['ip', 'netns', 'exec', netns, 'ip', '-j', 'addr', 'show'],
f"netns-{netns}-ip-addr-show.json")

def get_netns_list():
"""Fetch a list of network namespaces"""
return run_json_cmd(['ip', '-j', 'netns', 'list'],
"netns-list.json")

def netns_find_ifname(ifname):
"""Find which network namespace owns ifname (if any)"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if 'ifalias' in iface and iface['ifalias'] == ifname:
return netns['name']
return None

def netns_ifindex_to_ifname(ifindex):
"""Look through all network namespaces for an interface index and return its name"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if iface['ifindex'] == ifindex:
if 'ifalias' in iface:
return iface['ifalias']
if 'ifname' in iface:
return iface['ifname']
return None

return None

def add_ip_link(ifname, iface_in, iface_out):
if 'ifname' in iface_in:
iface_out['name'] = iface_in['ifname']
iface_out['name'] = ifname

if 'ifindex' in iface_in:
iface_out['if-index'] = iface_in['ifindex']
Expand All @@ -664,8 +698,14 @@ def add_ip_link(ifname, iface_in, iface_out):

multicast = get_brport_multicast(ifname)
insert(iface_out, "infix-interfaces:bridge-port", "multicast", multicast)
if 'link' in iface_in and not iface_is_dsa(iface_in):
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", iface_in['link'])
if not iface_is_dsa(iface_in):
if 'link' in iface_in:
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", iface_in['link'])
elif 'link_index' in iface_in:
# 'link_index' is the only reference we have if the link iface is in a namespace
lower = netns_ifindex_to_ifname(iface_in['link_index'])
if lower:
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", lower)

if 'flags' in iface_in:
iface_out['admin-status'] = "up" if "UP" in iface_in['flags'] else "down"
Expand Down Expand Up @@ -885,6 +925,39 @@ def add_mdb_to_bridge(brname, iface_out, mc_status):
insert(iface_out, "infix-interfaces:bridge", "multicast", multicast)
insert(iface_out, "infix-interfaces:bridge", "multicast-filters", "multicast-filter", multicast_filters)

def add_container_ifaces(yang_ifaces):
"""Add all podman interfaces with limited data"""
interfaces={}
try:
containers = run_json_cmd(['podman', 'ps', '--format', 'json'], "podman-ps.json", default=[])
except Exception as e:
logging.error(f"Error, unable to run podman: {e}")
return

for container in containers:
name = container.get('Names', ['Unknown'])[0]
networks = container.get('Networks', [])

for network in networks:
if not network in interfaces:
interfaces[network] = []
if name not in interfaces[network]:
interfaces[network].append(name)

for ifname, containers in interfaces.items():
iface_out = {}
iface_out['name'] = ifname
iface_out['type'] = "infix-if-type:other" # Fallback
insert(iface_out, "infix-interfaces:container-network", "containers", containers)

netns = netns_find_ifname(ifname)
if netns is not None:
ip_link_data = netns_get_ip_link(netns)
ip_link_data = next((d for d in ip_link_data if d.get('ifalias') == ifname), None)
add_ip_link(ifname, ip_link_data, iface_out)

yang_ifaces.append(iface_out)

# Helper function to add tagged/untagged interfaces to a vlan dict in a list
def _add_vlan_iface(vlans, multicast_filter, multicast, vid, key, val):
for d in vlans:
Expand Down Expand Up @@ -962,6 +1035,8 @@ def add_interface(ifname, yang_ifaces):
addr = next((d for d in ip_addr_data if d.get('ifname') == link["ifname"]), None)
_add_interface(link["ifname"], link, addr, yang_ifaces)

add_container_ifaces(yang_ifaces)

def main():
global TESTPATH
global logger
Expand Down
6 changes: 6 additions & 0 deletions test/case/cli/cli-output/show-interfaces.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ br0 bridge  vlan:40u,50t
br1 bridge  
│ ethernet UP 02:00:00:00:00:02
└ e2 bridge FORWARDING vlan:30u pvid:30
e2 container system 
e3 ethernet UP 02:00:00:00:00:03
e4 ethernet DOWN 02:00:00:00:00:04
veth0b container system 
veth0j ethernet UP b2:82:e3:ce:d5:9e
veth peer:veth0k
ipv4 192.168.1.1/24 (static)
veth0k container system2 
32 changes: 32 additions & 0 deletions test/case/cli/system-output/ip-addr-show.json
Original file line number Diff line number Diff line change
Expand Up @@ -625,5 +625,37 @@
"collisions": 0
}
}
},
{
"ifindex": 9,
"link_index": 8,
"ifname": "veth0j",
"flags": [
"BROADCAST",
"MULTICAST",
"UP",
"LOWER_UP"
],
"mtu": 1500,
"qdisc": "noqueue",
"operstate": "UP",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "b2:82:e3:ce:d5:9e",
"broadcast": "ff:ff:ff:ff:ff:ff",
"link_netnsid": 1,
"addr_info": [
{
"family": "inet",
"local": "192.168.1.1",
"prefixlen": 24,
"scope": "global",
"protocol": "static",
"label": "veth0j",
"valid_life_time": 4294967295,
"preferred_life_time": 4294967295
}
]
}
]
56 changes: 56 additions & 0 deletions test/case/cli/system-output/ip-link-show.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,5 +618,61 @@
"collisions": 0
}
}
},
{
"ifindex": 9,
"link_index": 8,
"ifname": "veth0j",
"flags": [
"BROADCAST",
"MULTICAST",
"UP",
"LOWER_UP"
],
"mtu": 1500,
"qdisc": "noqueue",
"operstate": "UP",
"linkmode": "DEFAULT",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "b2:82:e3:ce:d5:9e",
"broadcast": "ff:ff:ff:ff:ff:ff",
"link_netnsid": 1,
"promiscuity": 0,
"allmulti": 0,
"min_mtu": 68,
"max_mtu": 65535,
"linkinfo": {
"info_kind": "veth"
},
"inet6_addr_gen_mode": "none",
"num_tx_queues": 1,
"num_rx_queues": 1,
"gso_max_size": 65536,
"gso_max_segs": 65535,
"tso_max_size": 524280,
"tso_max_segs": 65535,
"gro_max_size": 65536,
"gso_ipv4_max_size": 65536,
"gro_ipv4_max_size": 65536,
"stats64": {
"rx": {
"bytes": 1006,
"packets": 13,
"errors": 0,
"dropped": 0,
"over_errors": 0,
"multicast": 0
},
"tx": {
"bytes": 18668,
"packets": 50,
"errors": 0,
"dropped": 0,
"carrier_errors": 0,
"collisions": 0
}
}
}
]
Loading

0 comments on commit b9492c7

Please sign in to comment.