Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Add container interfaces to show interfaces #783

Merged
merged 2 commits into from
Oct 31, 2024
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
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
93 changes: 81 additions & 12 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,17 +698,17 @@ 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:
admin_xlate = {
"UP": "up",
"DOWN": "down"
}

admin_status = admin_xlate.get("UP" if "UP" in iface_in['flags'] else "DOWN", "testing")
iface_out['admin-status'] = admin_status
iface_out['admin-status'] = "up" if "UP" in iface_in['flags'] else "down"

if 'operstate' in iface_in:
xlate = {
Expand Down Expand Up @@ -891,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 @@ -968,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