@@ -1,9 +1,10 @@
""" Unit tests for LinuxDriver – parsing helpers (no real SSH connection needed). """
import re
from unittest . mock import MagicMock , patch
import pytest
from unittest . mock import MagicMock , patch
from napalm_linux . linux import (
_YUM_SECURITY_COMMAND ,
LinuxDriver ,
@@ -11,7 +12,6 @@ from napalm_linux.linux import (
_split_status ,
)
# ---------------------------------------------------------------------------
# Fixture – driver without a real connection
# ---------------------------------------------------------------------------
@@ -327,11 +327,7 @@ PASSWD_OUT = (
" daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin \n "
)
GROUP_OUT = (
" sudo:x:27:admin \n "
" docker:x:999:admin \n "
" adm:x:4:admin \n "
)
GROUP_OUT = " sudo:x:27:admin \n docker:x:999:admin \n adm:x:4:admin \n "
def test_get_os_users ( driver ) :
@@ -477,8 +473,7 @@ DNF_UPGRADE_SUCCESS = (
)
DNF_UPGRADE_ERROR = (
" Last metadata expiration check: 0:01:23 ago. \n "
" Error: No match for argument: nonexistent-pkg \n "
" Last metadata expiration check: 0:01:23 ago. \n Error: No match for argument: nonexistent-pkg \n "
)
@@ -532,6 +527,7 @@ def test_type_label_is_linux():
def test_type_label_overrides_os_driver ( ) :
from napalm_device_types import OSDriver
assert OSDriver . TYPE_LABEL == " OS "
assert LinuxDriver . TYPE_LABEL != OSDriver . TYPE_LABEL
@@ -555,18 +551,31 @@ def _dmi_output(
# Mirrors the real shell output: DMIBEGIN sentinel followed by 9 fields.
# The sentinel prevents _send()'s .strip() from eating leading blank lines
# (which occur on ARM where all DMI files are absent).
return " \n " . join ( [
" DMIBEGIN " ,
sys_vendor , product_name , product_version , product_serial ,
product_uuid , detect_virt , dt_model , cpuinfo_serial , cpuinfo_model ,
] )
return " \n " . join (
[
" DMIBEGIN " ,
sys_vendor ,
product_name ,
product_version ,
product_serial ,
product_uuid ,
detect_virt ,
dt_model ,
cpuinfo_serial ,
cpuinfo_model ,
]
)
class TestCollectPlatformInfo :
def test_baremetal_dell ( self , driver ) :
raw = _dmi_output (
" Dell Inc. " , " PowerEdge R720 " , " Not Specified " , " ABC123 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " , " none " ,
" Dell Inc. " ,
" PowerEdge R720 " ,
" Not Specified " ,
" ABC123 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " ,
" none " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -577,8 +586,12 @@ class TestCollectPlatformInfo:
def test_baremetal_lenovo_product_version_preferred ( self , driver ) :
raw = _dmi_output (
" LENOVO " , " 10M8000VUS " , " ThinkCentre M910x " , " MP1234 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " , " none " ,
" LENOVO " ,
" 10M8000VUS " ,
" ThinkCentre M910x " ,
" MP1234 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " ,
" none " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -589,8 +602,12 @@ class TestCollectPlatformInfo:
def test_vm_kvm ( self , driver ) :
raw = _dmi_output (
" QEMU " , " Standard PC (i440FX + PIIX, 1996) " , " pc-i440fx-9.1 " , " " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " , " kvm " ,
" QEMU " ,
" Standard PC (i440FX + PIIX, 1996) " ,
" pc-i440fx-9.1 " ,
" " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " ,
" kvm " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -601,8 +618,12 @@ class TestCollectPlatformInfo:
def test_vm_vmware ( self , driver ) :
raw = _dmi_output (
" VMware, Inc. " , " VMware Virtual Platform " , " None " , " VMware-42 12 34 56 " ,
" 4244560c-dead-beef-0000-abcdef123456 " , " vmware " ,
" VMware, Inc. " ,
" VMware Virtual Platform " ,
" None " ,
" VMware-42 12 34 56 " ,
" 4244560c-dead-beef-0000-abcdef123456 " ,
" vmware " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -613,8 +634,12 @@ class TestCollectPlatformInfo:
def test_vm_hyperv ( self , driver ) :
raw = _dmi_output (
" Microsoft Corporation " , " Virtual Machine " , " Hyper-V UEFI Release v4.1 " , " " ,
" 7C5B4B1F-1234-5678-ABCD-000000000001 " , " microsoft " ,
" Microsoft Corporation " ,
" Virtual Machine " ,
" Hyper-V UEFI Release v4.1 " ,
" " ,
" 7C5B4B1F-1234-5678-ABCD-000000000001 " ,
" microsoft " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -625,8 +650,12 @@ class TestCollectPlatformInfo:
def test_junk_dmi_values_filtered ( self , driver ) :
raw = _dmi_output (
" To Be Filled By O.E.M. " , " To Be Filled By O.E.M. " , " Not Specified " ,
" To Be Filled By O.E.M. " , " " , " none " ,
" To Be Filled By O.E.M. " ,
" To Be Filled By O.E.M. " ,
" Not Specified " ,
" To Be Filled By O.E.M. " ,
" " ,
" none " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -637,8 +666,12 @@ class TestCollectPlatformInfo:
def test_vm_kvm_fallback_via_dmi_when_detect_virt_unavailable ( self , driver ) :
# systemd-detect-virt returns "none" (not installed), sys_vendor reveals QEMU
raw = _dmi_output (
" QEMU " , " Standard PC (i440FX + PIIX, 1996) " , " " , " " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " , " none " ,
" QEMU " ,
" Standard PC (i440FX + PIIX, 1996) " ,
" " ,
" " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " ,
" none " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -648,8 +681,12 @@ class TestCollectPlatformInfo:
def test_container_docker ( self , driver ) :
raw = _dmi_output (
" QEMU " , " Standard PC (i440FX + PIIX, 1996) " , " " , " " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " , " docker " ,
" QEMU " ,
" Standard PC (i440FX + PIIX, 1996) " ,
" " ,
" " ,
" 4c4c4544-0000-2010-8020-b4c04f534a31 " ,
" docker " ,
)
with patch . object ( driver , " _send " , return_value = raw ) :
info = driver . _collect_platform_info ( )
@@ -674,7 +711,12 @@ class TestCollectPlatformInfo:
def test_arm_device_tree_fallback ( self , driver ) :
raw = _dmi_output (
" " , " " , " " , " " , " " , " none " ,
" " ,
" " ,
" " ,
" " ,
" " ,
" none " ,
dt_model = " Raspberry Pi 4 Model B Rev 1.4 " ,
cpuinfo_serial = " 100000002a6d96dc " ,
cpuinfo_model = " Raspberry Pi 4 Model B Rev 1.4 " ,
@@ -688,7 +730,12 @@ class TestCollectPlatformInfo:
def test_arm_cpuinfo_model_when_no_dt ( self , driver ) :
raw = _dmi_output (
" " , " " , " " , " " , " " , " none " ,
" " ,
" " ,
" " ,
" " ,
" " ,
" none " ,
dt_model = " " ,
cpuinfo_serial = " 0000000012345678 " ,
cpuinfo_model = " Raspberry Pi 3 Model B Plus Rev 1.3 " ,
@@ -713,7 +760,12 @@ class TestCollectPlatformInfo:
# d="none\nnone" which shifted subsequent lines off by one.
# With the ${d:-none} fix this no longer happens.
raw = _dmi_output (
" " , " " , " " , " " , " " , " none " ,
" " ,
" " ,
" " ,
" " ,
" " ,
" none " ,
dt_model = " " ,
cpuinfo_serial = " 0000000012345678 " ,
cpuinfo_model = " Raspberry Pi 3 Model B Rev 1.2 " ,
@@ -726,8 +778,12 @@ class TestCollectPlatformInfo:
def test_arm_fallback_ignored_when_dmi_present ( self , driver ) :
raw = _dmi_output (
" Dell Inc. " , " PowerEdge R720 " , " Not Specified " , " XYZ999 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " , " none " ,
" Dell Inc. " ,
" PowerEdge R720 " ,
" Not Specified " ,
" XYZ999 " ,
" 8a2e3f00-dead-beef-0000-123456789abc " ,
" none " ,
dt_model = " some-dt-model " ,
cpuinfo_serial = " deadbeef " ,
cpuinfo_model = " some cpuinfo model " ,
@@ -744,20 +800,23 @@ class TestCollectPlatformInfo:
# ---------------------------------------------------------------------------
@pytest.mark.parametrize ( " model,expected " , [
( " Raspberry Pi 4 Model B Rev 1.4 " , " Raspberry Pi Foundation " ) ,
( " Raspberry Pi 3 Model B Rev 1.2 " , " Raspberry Pi Foundation " ) ,
( " ODROID-N2L " , " Hardkernel " ) ,
( " NVIDIA Jetson Nano Developer Kit " , " NVIDIA " ) ,
( " Rock Pi 4C " , " Radxa " ) ,
( " Orange Pi 5 Plus " , " Xunlong Software " ) ,
( " Banana Pi BPI-R3 " , " SinoVoip " ) ,
( " NanoPi R4S " , " FriendlyElec " ) ,
( " PINE64 RockPro64 " , " Pine64 " ) ,
( " BeagleBone Black " , " BeagleBoard.org " ) ,
( " Unknown Board 1.0 " , " Unknown Board " ) ,
( " SomeSingleWordBoard " , " SomeSingleWordBoard " ) ,
] )
@pytest.mark.parametrize (
" model,expected " ,
[
( " Raspberry Pi 4 Model B Rev 1.4 " , " Raspberry Pi Foundation " ) ,
( " Raspberry Pi 3 Model B Rev 1.2 " , " Raspberry Pi Foundation " ) ,
( " ODROID-N2L " , " Hardkernel " ) ,
( " NVIDIA Jetson Nano Developer Kit " , " NVIDIA " ) ,
( " Rock Pi 4C " , " Radxa " ) ,
( " Orange Pi 5 Plus " , " Xunlong Software " ) ,
( " Banana Pi BPI-R3 " , " SinoVoip " ) ,
( " NanoPi R4S " , " FriendlyElec " ) ,
( " PINE64 RockPro64 " , " Pine64 " ) ,
( " BeagleBone Black " , " BeagleBoard.org " ) ,
( " Unknown Board 1.0 " , " Unknown Board " ) ,
( " SomeSingleWordBoard " , " SomeSingleWordBoard " ) ,
] ,
)
def test_arm_vendor_from_model ( model , expected ) :
assert _arm_vendor_from_model ( model ) == expected
@@ -768,11 +827,27 @@ def test_arm_vendor_from_model(model, expected):
def test_get_facts_baremetal_vendor_model_serial ( driver ) :
platform = { " vendor " : " Dell Inc. " , " model " : " PowerEdge R720 " , " serial " : " ABC123 " , " is_vm " : False }
with patch . object ( driver , " _collect_platform_info " , return_value = platform ) , \
patch . object ( driver , " _parse_uptime " , return_value = 86400 ) , \
patch . object ( driver , " _send " , side_effect = [ " myhost " , " myhost.example.com " , " Debian GNU/Linux 12 " , " eth0 \n eth1 " ,
" 6.1.0-18-amd64 " ] ) :
platform = {
" vendor " : " Dell Inc. " ,
" model " : " PowerEdge R720 " ,
" serial " : " ABC123 " ,
" is_vm " : False ,
}
with (
patch . object ( driver , " _collect_platform_info " , return_value = platform ) ,
patch . object ( driver , " _parse_uptime " , return_value = 86400 ) ,
patch . object (
driver ,
" _send " ,
side_effect = [
" myhost " ,
" myhost.example.com " ,
" Debian GNU/Linux 12 " ,
" eth0 \n eth1 " ,
" 6.1.0-18-amd64 " ,
] ,
) ,
) :
facts = driver . get_facts ( )
assert facts [ " vendor " ] == " Dell Inc. "
assert facts [ " model " ] == " PowerEdge R720 "
@@ -785,13 +860,20 @@ def test_get_facts_baremetal_vendor_model_serial(driver):
def test_get_facts_vm_kvm ( driver ) :
platform = {
" vendor " : " KVM " , " model " : " Virtual Machine " ,
" serial " : " 4c4c4544-0000-2010-8020-b4c04f534a31 " , " is_vm " : True ,
" vendor " : " KVM " ,
" model " : " Virtual Machine " ,
" serial " : " 4c4c4544-0000-2010-8020-b4c04f534a31 " ,
" is_vm " : True ,
}
with patch . object ( driver , " _collect_platform_info " , return_value = platform ) , \
patch . object ( driver , " _parse_uptime " , return_value = 3600 ) , \
patch . object ( driver , " _send " , side_effect = [ " vmhost " , " vmhost.local " , " Ubuntu 22.04 LTS " , " eth0 " ,
" 5.15.0-91-generic " ] ) :
with (
patch . object ( driver , " _collect_platform_info " , return_value = platform ) ,
patch . object ( driver , " _parse_uptime " , return_value = 3600 ) ,
patch . object (
driver ,
" _send " ,
side_effect = [ " vmhost " , " vmhost.local " , " Ubuntu 22.04 LTS " , " eth0 " , " 5.15.0-91-generic " ] ,
) ,
) :
facts = driver . get_facts ( )
assert facts [ " vendor " ] == " KVM "
assert facts [ " model " ] == " Virtual Machine "
@@ -800,9 +882,15 @@ def test_get_facts_vm_kvm(driver):
def test_get_facts_fallback_vendor_when_dmi_empty ( driver ) :
platform = { " vendor " : " " , " model " : " " , " serial " : " " , " is_vm " : False }
with patch . object ( driver , " _collect_platform_info " , return_value = platform ) , \
patch . object ( driver , " _parse_uptime " , return_value = 0 ) , \
patch . object ( driver , " _send " , side_effect = [ " host " , " host.local " , " Alpine Linux 3.19 " , " eth0 " , " 6.6.7-0-lts " ] ) :
with (
patch . object ( driver , " _collect_platform_info " , return_value = platform ) ,
patch . object ( driver , " _parse_uptime " , return_value = 0 ) ,
patch . object (
driver ,
" _send " ,
side_effect = [ " host " , " host.local " , " Alpine Linux 3.19 " , " eth0 " , " 6.6.7-0-lts " ] ,
) ,
) :
facts = driver . get_facts ( )
assert facts [ " vendor " ] == " Linux " # fallback to VENDOR class attribute
@@ -813,7 +901,12 @@ class TestNoDockerOfItsOwn:
only the fix that lets the login user use Docker stays. """
def test_the_cli_methods_are_gone ( self ) :
for name in ( " get_docker_info " , " get_docker_outdated " , " reconstruct_docker_run " , " _docker_bin " ) :
for name in (
" get_docker_info " ,
" get_docker_outdated " ,
" reconstruct_docker_run " ,
" _docker_bin " ,
) :
assert not hasattr ( LinuxDriver , name ) , name
def test_the_engine_access_and_the_permission_fix_stay ( self ) :
@@ -1019,7 +1112,9 @@ class TestReadToTheEnd:
def test_the_echoed_command_does_not_count_as_the_marker ( self , driver ) :
""" Its literal ``$?`` is no number. """
channel = _Channel ( [ " sudo true; echo __NETORK_RC=$? \n " , " __NETORK_RC=0 \n chris@vault-01:~$ " ] )
channel = _Channel (
[ " sudo true; echo __NETORK_RC=$? \n " , " __NETORK_RC=0 \n chris@vault-01:~$ " ]
)
driver . _device = channel
assert _split_status ( driver . _send ( _TRUE_WITH_STATUS ) ) [ 1 ] == 0
@@ -1043,7 +1138,7 @@ class TestReadToTheEnd:
)
def test_every_status_marker_is_waited_for ( self , driver , command ) :
marker = re . search ( r " echo (__[A-Z_]+=) " , command ) . group ( 1 )
channel = _Channel ( [ f " x <a@b> \n " , f " { marker } 0 \n chris@host:~$ " ] )
channel = _Channel ( [ " x <a@b> \n " , f " { marker } 0 \n chris@host:~$ " ] )
driver . _device = channel
output = driver . _send ( command )
@@ -1167,7 +1262,11 @@ def _kernel_wire(report: str) -> str:
import base64
import gzip
return " KFACTS_BEGIN \n " + base64 . encodebytes ( gzip . compress ( report . encode ( ) ) ) . decode ( ) + " KFACTS_END "
return (
" KFACTS_BEGIN \n "
+ base64 . encodebytes ( gzip . compress ( report . encode ( ) ) ) . decode ( )
+ " KFACTS_END "
)
def test_get_kernel_facts_carries_the_shared_command_across ( driver ) :
@@ -1369,12 +1468,16 @@ class TestAvailableUpdates:
driver . _pkg_manager = " dnf "
driver . _device . send_command . side_effect = [
" 0 " , # id -u
( " openssl-libs.x86_64 1:3.1.4-2.fc40 updates \n "
" vim-enhanced.x86_64 2:9.1.083-1 .fc40 updates\n __NETORK_RC=100 " ) ,
(
" openssl-libs.x86_64 1:3.1.4-2 .fc40 updates\n "
" vim-enhanced.x86_64 2:9.1.083-1.fc40 updates \n __NETORK_RC=100 "
) ,
# DNF_SECURITY_COMMAND pipes through cat: the outer status is cat's, dnf's own
# is in the __DNF_RC marker.
( " FEDORA-2024-1a2b3c4d5e Important/Sec. openssl-libs-1:3.1.4-2.fc40.x86_64 \n "
" __DNF_RC=0 \n __NETORK_RC=0 " ) ,
(
" FEDORA-2024-1a2b3c4d5e Important/Sec. openssl-libs-1:3.1.4-2.fc40.x86_64 \n "
" __DNF_RC=0 \n __NETORK_RC=0 "
) ,
]
updates = { u [ " name " ] : u for u in driver . get_available_updates ( ) }
@@ -1402,10 +1505,14 @@ class TestAvailableUpdates:
driver . _pkg_manager = " yum "
driver . _device . send_command . side_effect = [
" 0 " , # id -u
( " openssl-libs.x86_64 1:1.0.2k-26.el7_9 updates \n "
" vim-enhanced.x86_64 2:7.4.629-8.el7_9 updates \n __NETORK_RC=100 " ) ,
( " RHSA-2023:3556 Important/Sec. openssl-libs-1:1.0.2k-26.el7_9.x86_64 \n "
" __DNF_RC=0 \n __NETORK_RC=0 " ) ,
(
" openssl-libs.x86_64 1:1.0.2k-26.el7_9 updates \n "
" vim-enhanced.x86_64 2:7.4.629-8.el7_9 updates \n __NETORK_RC=100 "
) ,
(
" RHSA-2023:3556 Important/Sec. openssl-libs-1:1.0.2k-26.el7_9.x86_64 \n "
" __DNF_RC=0 \n __NETORK_RC=0 "
) ,
]
updates = { u [ " name " ] : u for u in driver . get_available_updates ( ) }
@@ -1449,7 +1556,10 @@ class TestAvailableUpdates:
def test_dnf_that_failed_raises ( self , driver ) :
driver . _pkg_manager = " dnf "
driver . _device . send_command . side_effect = [ " 0 " , " Error: Failed to download metadata \n __NETORK_RC=1 " ]
driver . _device . send_command . side_effect = [
" 0 " ,
" Error: Failed to download metadata \n __NETORK_RC=1 " ,
]
with pytest . raises ( RuntimeError ) :
driver . get_available_updates ( )
@@ -1461,7 +1571,10 @@ class TestRefreshAvailableUpdates:
def test_apt_refreshes_its_index_as_root ( self , driver ) :
driver . _pkg_manager = " apt "
driver . _device . send_command . side_effect = [ " 0 " , " Hit:1 http://archive.ubuntu.com noble InRelease \n __NETORK_RC=0 " ]
driver . _device . send_command . side_effect = [
" 0 " ,
" Hit:1 http://archive.ubuntu.com noble InRelease \n __NETORK_RC=0 " ,
]
result = driver . refresh_available_updates ( )
@@ -1470,7 +1583,10 @@ class TestRefreshAvailableUpdates:
def test_without_a_sudo_password_it_never_waits_for_one ( self , driver ) :
driver . _pkg_manager = " apt "
driver . _device . send_command . side_effect = [ " 1000 " , " sudo: a password is required \n __NETORK_RC=1 " ]
driver . _device . send_command . side_effect = [
" 1000 " ,
" sudo: a password is required \n __NETORK_RC=1 " ,
]
result = driver . refresh_available_updates ( )