@@ -1390,6 +1390,10 @@ def get_port_assignments_configuration(self, network_element, filters):
13901390 # The device must pass ALL specified filters to be included.
13911391 # Evaluation order: device_ips → serial_numbers → hostnames.
13921392 # Within each filter type, matching is OR (any value match).
1393+ # Note: 'serial_numbers' additionally handles stack switches.
1394+ # Catalyst Center returns 'serialNumber' as a comma-separated
1395+ # string for stacks; the device matches if ANY member serial
1396+ # is in the filter set.
13931397 # Omitted/empty filter type → no restriction on that attribute.
13941398 # ----------------------------------------------------------
13951399
@@ -1407,13 +1411,16 @@ def get_port_assignments_configuration(self, network_element, filters):
14071411
14081412 if fabric_site_name_serial_number_mapping :
14091413 expected_serials = fabric_site_name_serial_number_mapping .get (fabric_site_name , set ())
1410- if expected_serials and serial_number not in expected_serials :
1414+ matched , device_serials = self ._device_matches_serial_filter (
1415+ serial_number , expected_serials
1416+ )
1417+ if expected_serials and not matched :
14111418 self .log (
1412- f"Warning: Resolved serial number ' { serial_number } ' for "
1413- f"device ID '{ network_device_id } ' does not match expected "
1414- f"serial numbers { expected_serials } from filters for "
1415- f"fabric site '{ fabric_site_name } '." ,
1416- "DEBUG"
1419+ f"Resolved serial number(s) { device_serials or repr ( serial_number ) } "
1420+ f"for device ID '{ network_device_id } ' do not match expected "
1421+ f"serial numbers { expected_serials } from filters for fabric site "
1422+ f"'{ fabric_site_name } '." ,
1423+ "DEBUG" ,
14171424 )
14181425 continue
14191426
@@ -1753,6 +1760,10 @@ def get_port_channels_configuration(self, network_element, filters):
17531760 # The device must pass ALL specified filters to be included.
17541761 # Evaluation order: device_ips → serial_numbers → hostnames.
17551762 # Within each filter type, matching is OR (any value match).
1763+ # Note: 'serial_numbers' additionally handles stack switches.
1764+ # Catalyst Center returns 'serialNumber' as a comma-separated
1765+ # string for stacks; the device matches if ANY member serial
1766+ # is in the filter set.
17561767 # Omitted/empty filter type → no restriction on that attribute.
17571768 # ----------------------------------------------------------
17581769
@@ -1770,13 +1781,16 @@ def get_port_channels_configuration(self, network_element, filters):
17701781
17711782 if fabric_site_name_serial_number_mapping :
17721783 expected_serials = fabric_site_name_serial_number_mapping .get (fabric_site_name , set ())
1773- if expected_serials and serial_number not in expected_serials :
1784+ matched , device_serials = self ._device_matches_serial_filter (
1785+ serial_number , expected_serials
1786+ )
1787+ if expected_serials and not matched :
17741788 self .log (
1775- f"Warning: Resolved serial number ' { serial_number } ' for "
1776- f"device ID '{ network_device_id } ' does not match expected "
1777- f"serial numbers { expected_serials } from filters for "
1778- f"fabric site '{ fabric_site_name } '." ,
1779- "DEBUG"
1789+ f"Resolved serial number(s) { device_serials or repr ( serial_number ) } "
1790+ f"for device ID '{ network_device_id } ' do not match expected "
1791+ f"serial numbers { expected_serials } from filters for fabric site "
1792+ f"'{ fabric_site_name } '." ,
1793+ "DEBUG" ,
17801794 )
17811795 continue
17821796
@@ -1819,6 +1833,34 @@ def get_port_channels_configuration(self, network_element, filters):
18191833 )
18201834 return all_fabric_port_channels_details
18211835
1836+ def _device_matches_serial_filter (self , serial_number , expected_serials ):
1837+ """
1838+ Check whether a device's serial number passes the expected-serials filter.
1839+
1840+ Catalyst Center returns 'serialNumber' as a comma-separated string for
1841+ stacked devices (e.g. 'FCW2415C14A, FCW2415C147'). The device is treated
1842+ as matching if ANY of its stack-member serials is in the expected set.
1843+
1844+ Args:
1845+ serial_number (str | None): Raw 'serialNumber' value from the device
1846+ response. May be None, empty, or comma-separated.
1847+ expected_serials (set[str]): Expected serial numbers for this device's
1848+ fabric site. An empty set means no restriction.
1849+
1850+ Returns:
1851+ tuple[bool, set[str]]: (matched, parsed_device_serials). When
1852+ expected_serials is empty, matched is True and parsed_device_serials
1853+ is the parsed set (useful for logging).
1854+ """
1855+ device_serials = {
1856+ s .strip () for s in (serial_number or "" ).split ("," ) if s .strip ()
1857+ }
1858+
1859+ if not expected_serials :
1860+ return True , device_serials
1861+
1862+ return bool (device_serials & expected_serials ), device_serials
1863+
18221864 def get_wireless_ssids_configuration (self , network_element , filters ):
18231865 """
18241866 Retrieves and transforms wireless SSID configurations from Catalyst Center.
0 commit comments