From 1d5a4eaf03b93b564f8db497dbea3555248885f9 Mon Sep 17 00:00:00 2001 From: Justin Oliver Date: Wed, 18 Feb 2026 19:17:05 +0000 Subject: [PATCH 1/2] Update platform base to support new port LED policy Signed-off-by: Justin Oliver --- sonic_led/led_control_base.py | 5 ++ sonic_platform_base/led_base.py | 93 +++++++++++++++++++++++++++++++++ sonic_platform_base/sfp_base.py | 50 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 sonic_platform_base/led_base.py diff --git a/sonic_led/led_control_base.py b/sonic_led/led_control_base.py index 9505f2a4a..b90cdc7f4 100644 --- a/sonic_led/led_control_base.py +++ b/sonic_led/led_control_base.py @@ -10,6 +10,11 @@ except ImportError as e: raise ImportError (str(e) + " - required module not found") +# +# Platform API V1 (Deprecated) +# This class is maintained for backward compatibility. New platforms should +# implement Platform API V2 (sonic_platform_base.led_base.LedBase). +# class LedControlBase(object): __metaclass__ = abc.ABCMeta diff --git a/sonic_platform_base/led_base.py b/sonic_platform_base/led_base.py new file mode 100644 index 000000000..875cb4f4e --- /dev/null +++ b/sonic_platform_base/led_base.py @@ -0,0 +1,93 @@ +""" + led_base.py + + Abstract base class for implementing a platform-specific class with which + to interact with a LED in SONiC +""" + +try: + from enum import Enum +except ImportError as e: + raise ImportError(str(e) + " - required module not found") from e + +class LedColor(Enum): + """ + Enumeration of LED colors + + These are the standard colors used by LEDs. + """ + OFF = "off" + GREEN = "green" + AMBER = "amber" + YELLOW = "amber" # YELLOW is an alias for AMBER + RED = "red" + BLUE = "blue" + +class LedBase: + """ + Abstract base class for interfacing with an LED + + This class represents a single physical LED that can be controlled. + """ + + def __init__(self): + pass + + def get_name(self): + """ + Retrieves the name of the LED + + Returns: + A string representing the name of the LED. This is a platform-specific + identifier that can be used for debugging purposes. + + Example: + "port1_led1", "osfp1_led2" + """ + raise NotImplementedError + + def get_color_capabilities(self): + """ + Retrieves the color capabilities of the LED + + Returns: + A list of LedColor enum values representing the colors this LED + can display. Platforms should include OFF in the capability list + for clarity and completeness. + + Example: + [LedColor.OFF, LedColor.GREEN, LedColor.AMBER] + [LedColor.OFF, LedColor.GREEN, LedColor.RED, LedColor.BLUE] + """ + raise NotImplementedError + + def set_color(self, color): + """ + Sets the color of the LED + + Args: + color: A LedColor enum value representing the desired color. + The color must be one of the values returned by + get_color_capabilities() + + Returns: + A boolean, True if the color was set successfully, False if not + + Raises: + ValueError: if the color is not supported by this LED + + Example: + led.set_color(LedColor.GREEN) # Set LED to green + led.set_color(LedColor.AMBER) # Set LED to amber + led.set_color(LedColor.OFF) # Turn LED off + """ + raise NotImplementedError + + def get_color(self): + """ + Retrieves the current color of the LED (optional method) + + Returns: + A LedColor enum value representing the current color of the LED + """ + raise NotImplementedError diff --git a/sonic_platform_base/sfp_base.py b/sonic_platform_base/sfp_base.py index 5d8487774..a22c036e8 100644 --- a/sonic_platform_base/sfp_base.py +++ b/sonic_platform_base/sfp_base.py @@ -75,6 +75,8 @@ def __init__(self, bank=0): # List of ThermalBase-derived objects representing all thermals # available on the SFP self._thermal_list = [] + # List of all LEDs associated with SFP port + self._led_list = [] self._bank = bank self._xcvr_api_factory = XcvrApiFactory(self.read_eeprom, self.write_eeprom) self._xcvr_api = None @@ -124,6 +126,54 @@ def get_thermal(self, index): return thermal + def get_num_leds(self): + """ + Retrieves the number of LEDs available for this SFP port + + Returns: + An integer, the number of LEDs available on this SFP port + Returns 0 if no controllable LEDs are available + """ + return len(self._led_list) + + def get_all_leds(self): + """ + Retrieves all LEDs available for this SFP port + + Returns: + A list of objects derived from LedBase representing all LEDs + available on this SFP port cage. The list is ordered with index 0 + being the leftmost LED (for horizontal orientation) or topmost LED + (for vertical orientation). + Returns empty list [] if no controllable LEDs are available + + Platforms are responsible for implementing this method if they want the + front-panel LED control daemon (ledd) to use platform LED policy V2. + """ + raise NotImplementedError + + def get_led(self, index): + """ + Retrieves LED represented by (0-based) index + + Args: + index: An integer, the index (0-based) of the LED to retrieve. + Index 0 represents the leftmost LED (horizontal) or + topmost LED (vertical orientation) + + Returns: + An object derived from LedBase representing the specified LED + """ + led = None + + try: + led = self._led_list[index] + except IndexError: + sys.stderr.write("LED index {} out of range (0-{})\n".format( + index, len(self._led_list)-1)) + + return led + def get_transceiver_info(self): """ Retrieves transceiver info of this SFP From 850b5565d236ad90075e9d896134cd1a3f6051b4 Mon Sep 17 00:00:00 2001 From: Justin Oliver Date: Wed, 18 Feb 2026 19:17:05 +0000 Subject: [PATCH 2/2] Update platform base to support new port LED policy Signed-off-by: Justin Oliver --- tests/led_base_test.py | 66 +++++++++++++++++++++++++++++++++++ tests/sfp_base_test.py | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/led_base_test.py create mode 100644 tests/sfp_base_test.py diff --git a/tests/led_base_test.py b/tests/led_base_test.py new file mode 100644 index 000000000..9778b3f45 --- /dev/null +++ b/tests/led_base_test.py @@ -0,0 +1,66 @@ +''' +Test LedBase / LedColor module +''' + +import pytest + +from sonic_platform_base.led_base import LedBase, LedColor + + +class TestLedColor: + ''' + Collection of LedColor tests + ''' + + @staticmethod + def test_color_values(): + ''' + Each color maps to its expected string value. + ''' + assert LedColor.OFF.value == 'off' + assert LedColor.GREEN.value == 'green' + assert LedColor.AMBER.value == 'amber' + assert LedColor.RED.value == 'red' + assert LedColor.BLUE.value == 'blue' + + @staticmethod + def test_yellow_is_alias_of_amber(): + ''' + YELLOW shares AMBER's value, so Python's Enum makes it an alias: + the two members are the same object. + ''' + assert LedColor.YELLOW is LedColor.AMBER + assert LedColor.YELLOW.value == 'amber' + + +class TestLedBase: + ''' + Collection of LedBase tests + ''' + + @staticmethod + def test_unimplemented_methods_raise(): + ''' + All abstract methods raise NotImplementedError by default. + ''' + led = LedBase() + not_implemented_methods = [ + (led.get_name,), + (led.get_color_capabilities,), + (led.set_color, LedColor.GREEN), + (led.get_color,), + ] + + for method in not_implemented_methods: + func = method[0] + args = method[1:] + with pytest.raises(NotImplementedError): + func(*args) + + @staticmethod + def test_init_no_args(): + ''' + Default constructor takes no args and does not raise. + ''' + # Should not raise + LedBase() diff --git a/tests/sfp_base_test.py b/tests/sfp_base_test.py new file mode 100644 index 000000000..35ed1943e --- /dev/null +++ b/tests/sfp_base_test.py @@ -0,0 +1,78 @@ +''' +Test SfpBase LED-related methods +''' + +import pytest +from unittest import mock + +from sonic_platform_base.sfp_base import SfpBase +from sonic_platform_base.led_base import LedBase + +def _make_led(name='led0'): + led = mock.MagicMock(spec=LedBase) + led.get_name.return_value = name + return led + +class TestSfpBaseLeds: + ''' + Coverage for the LED accessors added by the platform LED policy V2 patch. + ''' + + @staticmethod + def test_led_list_initialized_empty(): + ''' + SfpBase.__init__ must initialize self._led_list to an empty list so + that platforms inheriting the default get_num_leds() / get_led() + behavior do not blow up before they populate it. + ''' + sfp = SfpBase() + assert sfp._led_list == [] + assert sfp.get_num_leds() == 0 + + @staticmethod + def test_leds_with_populated_list(): + ''' + Returns the correct count and get_led(index) returns the matching LED + ''' + sfp = SfpBase() + leds = [_make_led('a'), _make_led('b'), _make_led('c')] + sfp._led_list = leds + + assert sfp.get_num_leds() == 3 + assert sfp.get_led(0) is leds[0] + assert sfp.get_led(1) is leds[1] + assert sfp.get_led(2) is leds[2] + + @staticmethod + def test_get_all_leds_raises_by_default(): + ''' + get_all_leds() raises NotImplementedError on the base class. This is + the platform's opt-in to LED policy V2 + ''' + sfp = SfpBase() + with pytest.raises(NotImplementedError): + sfp.get_all_leds() + + @staticmethod + def test_get_led_out_of_range_returns_none(capsys): + ''' + Out-of-range indexes return None and write a diagnostic to stderr + (matching the get_thermal() pattern). + ''' + sfp = SfpBase() + sfp._led_list = [_make_led('a')] + + assert sfp.get_led(5) is None + captured = capsys.readouterr() + assert 'LED index 5 out of range' in captured.err + + @staticmethod + def test_get_led_empty_list_returns_none(capsys): + ''' + With no LEDs registered, any index is out of range. + ''' + sfp = SfpBase() + + assert sfp.get_led(0) is None + captured = capsys.readouterr() + assert 'LED index 0 out of range' in captured.err