summaryrefslogtreecommitdiff
path: root/cloudinit/netinfo.py
diff options
context:
space:
mode:
authorJames Falcon <james.falcon@canonical.com>2022-01-26 19:40:06 -0600
committerGitHub <noreply@github.com>2022-01-26 19:40:06 -0600
commit223b23e2c428aff6c1e61f49d8e2edde77801a12 (patch)
treeb9642e2a00c55202809fec16874f44cb635f2ed8 /cloudinit/netinfo.py
parent0d364a29d2fe233fa31bcd6d129b1ca61be60e20 (diff)
downloadcloud-init-git-223b23e2c428aff6c1e61f49d8e2edde77801a12.tar.gz
Add json parsing of ip addr show (SC-723) (#1210)
When obtaining information from "ip addr", default to using "ip --json addr" rather than using regex to parse "ip addr show" as json is machine readable as less prone to error. Deprecate but leave fallback to use "ip addr" for older iproute2 tooling which does not support --json param. Fix regex parsing of "ip addr" to support peer addresses and metrics.
Diffstat (limited to 'cloudinit/netinfo.py')
-rw-r--r--cloudinit/netinfo.py97
1 files changed, 90 insertions, 7 deletions
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 74e6b35a..5eeeb967 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -8,8 +8,10 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import json
import re
from copy import copy, deepcopy
+from ipaddress import IPv4Network
from cloudinit import log as logging
from cloudinit import subp, util
@@ -18,13 +20,84 @@ from cloudinit.simpletable import SimpleTable
LOG = logging.getLogger()
-
+# Example netdev format:
+# {'eth0': {'hwaddr': '00:16:3e:16:db:54',
+# 'ipv4': [{'bcast': '10.85.130.255',
+# 'ip': '10.85.130.116',
+# 'mask': '255.255.255.0',
+# 'scope': 'global'}],
+# 'ipv6': [{'ip': 'fd42:baa2:3dd:17a:216:3eff:fe16:db54/64',
+# 'scope6': 'global'},
+# {'ip': 'fe80::216:3eff:fe16:db54/64', 'scope6': 'link'}],
+# 'up': True},
+# 'lo': {'hwaddr': '',
+# 'ipv4': [{'bcast': '',
+# 'ip': '127.0.0.1',
+# 'mask': '255.0.0.0',
+# 'scope': 'host'}],
+# 'ipv6': [{'ip': '::1/128', 'scope6': 'host'}],
+# 'up': True}}
DEFAULT_NETDEV_INFO = {"ipv4": [], "ipv6": [], "hwaddr": "", "up": False}
+def _netdev_info_iproute_json(ipaddr_json):
+ """Get network device dicts from ip route and ip link info.
+
+ ipaddr_json: Output string from 'ip --json addr' command.
+
+ Returns a dict of device info keyed by network device name containing
+ device configuration values.
+
+ Raises json.JSONDecodeError if json could not be decoded
+ """
+ ipaddr_data = json.loads(ipaddr_json)
+ devs = {}
+
+ for dev in ipaddr_data:
+ flags = dev["flags"] if "flags" in dev else []
+ address = dev["address"] if dev.get("link_type") == "ether" else ""
+ dev_info = {
+ "hwaddr": address,
+ "up": bool("UP" in flags and "LOWER_UP" in flags),
+ "ipv4": [],
+ "ipv6": [],
+ }
+ for addr in dev.get("addr_info", []):
+ if addr.get("family") == "inet":
+ mask = (
+ str(IPv4Network(f'0.0.0.0/{addr["prefixlen"]}').netmask)
+ if "prefixlen" in addr
+ else ""
+ )
+ parsed_addr = {
+ "ip": addr.get("local", ""),
+ "mask": mask,
+ "bcast": addr.get("broadcast", ""),
+ "scope": addr.get("scope", ""),
+ }
+ dev_info["ipv4"].append(parsed_addr)
+ elif addr["family"] == "inet6":
+ ip = addr.get("local", "")
+ # address here refers to a peer address, and according
+ # to "man 8 ip-address":
+ # If a peer address is specified, the local address cannot
+ # have a prefix length. The network prefix is associated
+ # with the peer rather than with the local address.
+ if ip and not addr.get("address"):
+ ip = f"{ip}/{addr.get('prefixlen', 64)}"
+ parsed_addr = {
+ "ip": ip,
+ "scope6": addr.get("scope", ""),
+ }
+ dev_info["ipv6"].append(parsed_addr)
+ devs[dev["ifname"]] = dev_info
+ return devs
+
+
def _netdev_info_iproute(ipaddr_out):
"""
- Get network device dicts from ip route and ip link info.
+ DEPRECATED: Only used on distros that don't support ip json output
+ Use _netdev_info_iproute_json() when possible.
@param ipaddr_out: Output string from 'ip addr show' command.
@@ -47,7 +120,10 @@ def _netdev_info_iproute(ipaddr_out):
}
elif "inet6" in line:
m = re.match(
- r"\s+inet6\s(?P<ip>\S+)\sscope\s(?P<scope6>\S+).*", line
+ r"\s+inet6\s(?P<ip>\S+)"
+ r"(\s(peer\s\S+))?"
+ r"\sscope\s(?P<scope6>\S+).*",
+ line,
)
if not m:
LOG.warning(
@@ -57,8 +133,10 @@ def _netdev_info_iproute(ipaddr_out):
devs[dev_name]["ipv6"].append(m.groupdict())
elif "inet" in line:
m = re.match(
- r"\s+inet\s(?P<cidr4>\S+)(\sbrd\s(?P<bcast>\S+))?\sscope\s"
- r"(?P<scope>\S+).*",
+ r"\s+inet\s(?P<cidr4>\S+)"
+ r"(\smetric\s(?P<metric>\d+))?"
+ r"(\sbrd\s(?P<bcast>\S+))?"
+ r"\sscope\s(?P<scope>\S+).*",
line,
)
if not m:
@@ -209,8 +287,13 @@ def netdev_info(empty=""):
devs = _netdev_info_ifconfig_netbsd(ifcfg_out)
elif subp.which("ip"):
# Try iproute first of all
- (ipaddr_out, _err) = subp.subp(["ip", "addr", "show"])
- devs = _netdev_info_iproute(ipaddr_out)
+ try:
+ (ipaddr_out, _err) = subp.subp(["ip", "--json", "addr"])
+ devs = _netdev_info_iproute_json(ipaddr_out)
+ except subp.ProcessExecutionError:
+ # Can be removed when "ip --json" is available everywhere
+ (ipaddr_out, _err) = subp.subp(["ip", "addr", "show"])
+ devs = _netdev_info_iproute(ipaddr_out)
elif subp.which("ifconfig"):
# Fall back to net-tools if iproute2 is not present
(ifcfg_out, _err) = subp.subp(["ifconfig", "-a"], rcs=[0, 1])