Skip to content

Commit 8ea9b11

Browse files
authored
Merge pull request #297 from mssonicbld/sonicbld/202603-merge
```<br>* ba17c7b - (HEAD -> 202603) Merge branch '202511' of https://github.com/sonic-net/sonic-utilities into 202603 (2026-03-27) [Sonic Automation] * b921a54 - (origin/202511) Fix migrate_sonic_packages() crash on symlink resolv.conf (#4380) (2026-03-21) [mssonicbld] * beeaad6 - fix: skip PORT_INGRESS/EGRESS_MIRROR_CAPABLE check for ERSPAN mirror sessions (#4368) (2026-03-17) [mssonicbld]<br>```
2 parents f5f55d5 + ba17c7b commit 8ea9b11

4 files changed

Lines changed: 74 additions & 31 deletions

File tree

config/main.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,25 +1168,27 @@ def interface_has_mirror_config(ctx, mirror_table, dst_port, src_port, direction
11681168

11691169

11701170
def is_port_mirror_capability_supported(direction, namespace=None):
1171-
""" Check if port mirror capability is supported for the given direction """
1171+
""" Check if port mirror capability is supported for the given direction.
1172+
1173+
PORT_INGRESS_MIRROR_CAPABLE / PORT_EGRESS_MIRROR_CAPABLE only apply to SPAN
1174+
(port mirror) sessions. Callers should not invoke this for ERSPAN sessions.
1175+
Absent STATE_DB keys (None) are treated as supported for backward compatibility
1176+
with platforms that do not populate the SWITCH_CAPABILITY table.
1177+
"""
11721178
state_db = SonicV2Connector(use_unix_socket_path=True, namespace=namespace)
11731179
state_db.connect(state_db.STATE_DB, False)
11741180
entry_name = "SWITCH_CAPABILITY|switch"
11751181

1176-
# If no direction is specified, check both ingress and egress capabilities
1177-
if not direction:
1178-
ingress_supported = state_db.get(state_db.STATE_DB, entry_name, "PORT_INGRESS_MIRROR_CAPABLE")
1179-
egress_supported = state_db.get(state_db.STATE_DB, entry_name, "PORT_EGRESS_MIRROR_CAPABLE")
1180-
return ingress_supported == "true" and egress_supported == "true"
1182+
directions_to_check = []
1183+
if not direction or direction in ['rx', 'both']:
1184+
directions_to_check.append("PORT_INGRESS_MIRROR_CAPABLE")
1185+
if not direction or direction in ['tx', 'both']:
1186+
directions_to_check.append("PORT_EGRESS_MIRROR_CAPABLE")
11811187

1182-
if direction in ['rx', 'both']:
1183-
ingress_supported = state_db.get(state_db.STATE_DB, entry_name, "PORT_INGRESS_MIRROR_CAPABLE")
1184-
if ingress_supported != "true":
1185-
return False
1186-
1187-
if direction in ['tx', 'both']:
1188-
egress_supported = state_db.get(state_db.STATE_DB, entry_name, "PORT_EGRESS_MIRROR_CAPABLE")
1189-
if egress_supported != "true":
1188+
for capability_key in directions_to_check:
1189+
value = state_db.get(state_db.STATE_DB, entry_name, capability_key)
1190+
# Treat absent key (None) as supported; only reject explicit "false"
1191+
if value is not None and value != "true":
11901192
return False
11911193

11921194
return True
@@ -1241,13 +1243,15 @@ def validate_mirror_session_config(config_db, session_name, dst_port, src_port,
12411243
if direction not in ['rx', 'tx', 'both']:
12421244
ctx.fail("Error: Direction {} is invalid".format(direction))
12431245

1244-
# Check port mirror capability before allowing configuration
1245-
# If direction is provided, check the specific direction
1246-
1247-
for ns in namespace_set:
1248-
if not is_port_mirror_capability_supported(direction, namespace=ns):
1249-
ctx.fail("Error: Port mirror direction '{}' is not supported by the ASIC".format(
1250-
direction if direction else 'both'))
1246+
# Check port mirror capability before allowing configuration.
1247+
# ERSPAN sessions (dst_port=None) use src/dst IPs, not ports; the
1248+
# PORT_INGRESS/EGRESS_MIRROR_CAPABLE flags only apply to SPAN sessions.
1249+
is_erspan = dst_port is None
1250+
if not is_erspan:
1251+
for ns in namespace_set:
1252+
if not is_port_mirror_capability_supported(direction, namespace=ns):
1253+
ctx.fail("Error: Port mirror direction '{}' is not supported by the ASIC".format(
1254+
direction if direction else 'both'))
12511255

12521256
return True
12531257

sonic_installer/main.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ def migrate_sonic_packages(bootloader, binary_image_version):
325325
DOCKERD_SOCK = "docker.sock"
326326
VAR_RUN_PATH = "/var/run/"
327327
RESOLV_CONF_FILE = os.path.join("etc", "resolv.conf")
328-
RESOLV_CONF_BACKUP_FILE = os.path.join("/", TMP_DIR, "resolv.conf.backup")
329328

330329
packages_file = "packages.json"
331330
packages_path = os.path.join(PACKAGE_MANAGER_DIR, packages_file)
@@ -382,8 +381,19 @@ def migrate_sonic_packages(bootloader, binary_image_version):
382381
os.path.join(VAR_RUN_PATH, DOCKERD_SOCK),
383382
os.path.join(new_image_mount, "tmp", DOCKERD_SOCK)])
384383

385-
run_command_or_raise(["cp", os.path.join(new_image_mount, RESOLV_CONF_FILE), RESOLV_CONF_BACKUP_FILE])
386-
run_command_or_raise(["cp", os.path.join("/", RESOLV_CONF_FILE), os.path.join(new_image_mount, RESOLV_CONF_FILE)])
384+
# Inject host DNS into chroot for sonic-package-manager to resolve hostnames.
385+
chroot_resolv = os.path.join(new_image_mount, RESOLV_CONF_FILE)
386+
if os.path.islink(chroot_resolv):
387+
# Symlink: populate the target inside the chroot so the symlink resolves.
388+
# Cannot cp over the symlink because the absolute target path escapes
389+
# the overlay mount to the host filesystem ("are the same file" error).
390+
resolv_target = os.readlink(chroot_resolv)
391+
chroot_target = os.path.join(new_image_mount, resolv_target.lstrip("/"))
392+
run_command_or_raise(["mkdir", "-p", os.path.dirname(chroot_target)])
393+
run_command_or_raise(["cp", "-L", os.path.join("/", RESOLV_CONF_FILE), chroot_target])
394+
else:
395+
# Regular file: overwrite with host DNS content.
396+
run_command_or_raise(["cp", os.path.join("/", RESOLV_CONF_FILE), chroot_resolv])
387397

388398
run_command_or_raise(["chroot", new_image_mount, "sh", "-c", "command -v {}".format(SONIC_PACKAGE_MANAGER)])
389399
run_command_or_raise(["chroot", new_image_mount, SONIC_PACKAGE_MANAGER, "migrate",
@@ -395,7 +405,6 @@ def migrate_sonic_packages(bootloader, binary_image_version):
395405
run_command_or_raise(["chroot", new_image_mount, DOCKER_CTL_SCRIPT, "stop"], raise_exception=False)
396406
if os.path.exists(docker_default_config_backup):
397407
run_command_or_raise(["mv", docker_default_config_backup, docker_default_config], raise_exception=False);
398-
run_command_or_raise(["cp", RESOLV_CONF_BACKUP_FILE, os.path.join(new_image_mount, RESOLV_CONF_FILE)], raise_exception=False)
399408
umount(new_image_mount, recursive=True, read_only=False, remove_dir=False, raise_exception=False)
400409
umount(new_image_mount, raise_exception=False)
401410

tests/config_mirror_session_test.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,18 @@ def test_mirror_session_capability_checking():
390390
assert result.exit_code != 0
391391
assert "Error: Port mirror direction 'both' is not supported by the ASIC" in result.output
392392

393+
# Test 2: ERSPAN sessions bypass capability check even when capability returns False
394+
with mock.patch('config.main.is_port_mirror_capability_supported') as mock_capability:
395+
mock_capability.return_value = False
396+
397+
result = runner.invoke(
398+
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
399+
["test_erspan", "1.1.1.1", "2.2.2.2", "8", "64", "0x88be"])
400+
401+
# ERSPAN should not be blocked by port mirror capability
402+
assert "is not supported by the ASIC" not in result.output
403+
mock_capability.assert_not_called()
404+
393405

394406
def test_mirror_session_capability_function():
395407
"""Test the is_port_mirror_capability_supported function directly"""
@@ -450,9 +462,9 @@ def test_mirror_session_capability_function():
450462
result = config.is_port_mirror_capability_supported("both")
451463
assert result is False
452464

453-
# Test no direction (should fail)
465+
# Test no direction (checks both ingress and egress)
454466
result = config.is_port_mirror_capability_supported(None)
455-
assert result is False
467+
assert result is False # egress is "false", so fails
456468

457469
# Test 3: Test with no capability support
458470
with mock.patch('config.main.SonicV2Connector') as mock_connector:
@@ -468,8 +480,24 @@ def test_mirror_session_capability_function():
468480
("SWITCH_CAPABILITY|switch", "PORT_EGRESS_MIRROR_CAPABLE"): "false"
469481
}.get((entry, field), "false")
470482

471-
# All directions should fail
483+
# SPAN directions should fail when explicitly set to "false"
472484
assert config.is_port_mirror_capability_supported("rx") is False
473485
assert config.is_port_mirror_capability_supported("tx") is False
474486
assert config.is_port_mirror_capability_supported("both") is False
487+
# direction=None checks both; both are "false" so fails
475488
assert config.is_port_mirror_capability_supported(None) is False
489+
490+
# Test 4: Test with absent capability keys (None returned from STATE_DB)
491+
with mock.patch('config.main.SonicV2Connector') as mock_connector:
492+
mock_instance = mock.Mock()
493+
mock_connector.return_value = mock_instance
494+
mock_instance.connect.return_value = None
495+
496+
# Simulate keys absent from STATE_DB (returns None)
497+
mock_instance.get.return_value = None
498+
499+
# All directions should return True (backward compatibility: absent = supported)
500+
assert config.is_port_mirror_capability_supported("rx") is True
501+
assert config.is_port_mirror_capability_supported("tx") is True
502+
assert config.is_port_mirror_capability_supported("both") is True
503+
assert config.is_port_mirror_capability_supported(None) is True

tests/test_sonic_installer.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def test_install(run_command, run_command_or_raise, get_bootloader, swap, fs):
3434
fs.create_dir(os.path.join(mounted_image_folder, "usr/lib/docker/docker.sh"))
3535
fs.create_file("/var/run/docker.pid", contents="15")
3636
fs.create_file("/proc/15/cmdline", contents="\x00".join(["dockerd"] + dockerd_opts))
37+
# Simulate new image: /etc/resolv.conf is a dangling symlink (target doesn't exist in squashfs)
38+
fs.create_symlink(f"{mounted_image_folder}/etc/resolv.conf", "/run/resolvconf/resolv.conf")
3739

3840
# Setup bootloader mock
3941
mock_bootloader = Mock()
@@ -91,12 +93,12 @@ def rootfs_path_mock(path):
9193
f"{mounted_image_folder}/var/lib/sonic-package-manager"]),
9294
call(["touch", f"{mounted_image_folder}/tmp/docker.sock"]),
9395
call(["mount", "--bind", "/var/run/docker.sock", f"{mounted_image_folder}/tmp/docker.sock"]),
94-
call(["cp", f"{mounted_image_folder}/etc/resolv.conf", "/tmp/resolv.conf.backup"]),
95-
call(["cp", "/etc/resolv.conf", f"{mounted_image_folder}/etc/resolv.conf"]),
96+
# /etc/resolv.conf is a dangling symlink -> /run/resolvconf/resolv.conf, populate its target
97+
call(["mkdir", "-p", f"{mounted_image_folder}/run/resolvconf"]),
98+
call(["cp", "-L", "/etc/resolv.conf", f"{mounted_image_folder}/run/resolvconf/resolv.conf"]),
9699
call(["chroot", mounted_image_folder, "sh", "-c", "command -v sonic-package-manager"]),
97100
call(["chroot", mounted_image_folder, "sonic-package-manager", "migrate", "/tmp/packages.json", "--dockerd-socket", "/tmp/docker.sock", "-y"], capture=False),
98101
call(["chroot", mounted_image_folder, "/usr/lib/docker/docker.sh", "stop"], raise_exception=False),
99-
call(["cp", "/tmp/resolv.conf.backup", f"{mounted_image_folder}/etc/resolv.conf"], raise_exception=False),
100102
call(["umount", "-f", "-R", mounted_image_folder], raise_exception=False),
101103
call(["umount", "-r", "-f", mounted_image_folder], raise_exception=False),
102104
call(["rm", "-rf", mounted_image_folder], raise_exception=False),

0 commit comments

Comments
 (0)