diff mbox series

[v3] Add support for PCIe SSD status LED management

Message ID 20210813213653.3760-1-stuart.w.hayes@gmail.com
State New
Headers show
Series [v3] Add support for PCIe SSD status LED management | expand

Commit Message

Stuart Hayes Aug. 13, 2021, 9:36 p.m. UTC
This patch adds support for the PCIe SSD Status LED Management
interface, as described in the "_DSM Additions for PCIe SSD Status LED
Management" ECN to the PCI Firmware Specification revision 3.2.

It will add (led_classdev) LEDs to each PCIe device that has a supported
_DSM interface (one off/on LED per supported state). Both PCIe storage
devices, and the ports to which they are connected, can support this
interface.

Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>
---
V2:
	* Simplified interface to a single "states" attribute under the LED
	  classdev using only state names
	* Reworked driver to separate _DSM specific code, so support for
	  NPEM (or other methods) could be easily be added
	* Use BIT macro
V3:
	* Changed code to use a single LED per supported state
	* Moved to drivers/pci/pcie
	* Changed Kconfig dependency to LEDS_CLASS instead of NEW_LEDS
	* Added PCI device class check before _DSM presence check
	* Other cleanups that don't affect the function

---
 drivers/pci/pcie/Kconfig    |  11 +
 drivers/pci/pcie/Makefile   |   1 +
 drivers/pci/pcie/ssd-leds.c | 419 ++++++++++++++++++++++++++++++++++++
 3 files changed, 431 insertions(+)
 create mode 100644 drivers/pci/pcie/ssd-leds.c

Comments

Lukas Wunner Aug. 14, 2021, 6:23 a.m. UTC | #1
On Fri, Aug 13, 2021 at 05:36:53PM -0400, Stuart Hayes wrote:
> +struct mutex drive_status_dev_list_lock;
> +struct list_head drive_status_dev_list;

Should be declared static.

> +const guid_t pcie_ssd_leds_dsm_guid =
> +	GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
> +		  0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);

Same.

> +struct drive_status_led_ops dsm_drive_status_led_ops = {
> +	.get_supported_states = get_supported_states_dsm,
> +	.get_current_states = get_current_states_dsm,
> +	.set_current_states = set_current_states_dsm,
> +};

Same.

> +static void probe_pdev(struct pci_dev *pdev)
> +{
> +	/*
> +	 * This is only supported on PCIe storage devices and PCIe ports
> +	 */
> +	if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
> +	    pdev->class != PCI_CLASS_BRIDGE_PCI)
> +		return;
> +	if (pdev_has_dsm(pdev))
> +		add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
> +}

Why is &dsm_drive_status_led_ops passed to add_drive_status_dev()?
It's always the same argument.

> +static int __init ssd_leds_init(void)
> +{
> +	mutex_init(&drive_status_dev_list_lock);
> +	INIT_LIST_HEAD(&drive_status_dev_list);
> +
> +	bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
> +	initial_scan_for_leds();
> +	return 0;
> +}

There's a concurrency issue here:  initial_scan_for_leds() uses
bus_for_each_dev(), which walks the bus's klist_devices.  When a
device is added (e.g. hotplugged), that device gets added to the
tail of klist_devices.  (See call to klist_add_tail() in
bus_add_device().)

It is thus possible that probe_pdev() is run concurrently for the
same device, once by the notifier and once by initial_scan_for_leds().

The problem is that add_drive_status_dev() only checks at the top
of the function whether the device has already been added to
drive_status_dev_list.  It goes on to instantiate the LED
and only then adds the device to drive_status_dev_list.

It's thus possible that the same LED is instantiated twice
which might confuse users.

Thanks,

Lukas
Pavel Machek Aug. 18, 2021, 7:05 a.m. UTC | #2
Hi!

> This patch adds support for the PCIe SSD Status LED Management
> interface, as described in the "_DSM Additions for PCIe SSD Status LED
> Management" ECN to the PCI Firmware Specification revision 3.2.
> 
> It will add (led_classdev) LEDs to each PCIe device that has a supported
> _DSM interface (one off/on LED per supported state). Both PCIe storage
> devices, and the ports to which they are connected, can support this
> interface.
> 
> Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>

I believe these are not LEDs, right? This is something that displays
information to the user, but how exactly it is implemented is up to
BIOS vendor.

I don't think it is good fit for LED subsystem.

Best regards,
								Pavel
Stuart Hayes Oct. 4, 2021, 5:41 p.m. UTC | #3
On 8/14/2021 1:23 AM, Lukas Wunner wrote:
> On Fri, Aug 13, 2021 at 05:36:53PM -0400, Stuart Hayes wrote:
>> +struct mutex drive_status_dev_list_lock;
>> +struct list_head drive_status_dev_list;
> 
> Should be declared static.
> 
>> +const guid_t pcie_ssd_leds_dsm_guid =
>> +	GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
>> +		  0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
> 
> Same.
> 
>> +struct drive_status_led_ops dsm_drive_status_led_ops = {
>> +	.get_supported_states = get_supported_states_dsm,
>> +	.get_current_states = get_current_states_dsm,
>> +	.set_current_states = set_current_states_dsm,
>> +};
> 
> Same.
> 

Thank you!

>> +static void probe_pdev(struct pci_dev *pdev)
>> +{
>> +	/*
>> +	 * This is only supported on PCIe storage devices and PCIe ports
>> +	 */
>> +	if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
>> +	    pdev->class != PCI_CLASS_BRIDGE_PCI)
>> +		return;
>> +	if (pdev_has_dsm(pdev))
>> +		add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
>> +}
> 
> Why is &dsm_drive_status_led_ops passed to add_drive_status_dev()?
> It's always the same argument.
>

Because I hope this will also support NPEM as well, since it is so 
similar except for using a PCIe extended capability instead of a _DSM 
method. This will make it very easy to add the support... I just don't 
have any NPEM hardware yet.

>> +static int __init ssd_leds_init(void)
>> +{
>> +	mutex_init(&drive_status_dev_list_lock);
>> +	INIT_LIST_HEAD(&drive_status_dev_list);
>> +
>> +	bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
>> +	initial_scan_for_leds();
>> +	return 0;
>> +}
> 
> There's a concurrency issue here:  initial_scan_for_leds() uses
> bus_for_each_dev(), which walks the bus's klist_devices.  When a
> device is added (e.g. hotplugged), that device gets added to the
> tail of klist_devices.  (See call to klist_add_tail() in
> bus_add_device().)
> 
> It is thus possible that probe_pdev() is run concurrently for the
> same device, once by the notifier and once by initial_scan_for_leds().
> 
> The problem is that add_drive_status_dev() only checks at the top
> of the function whether the device has already been added to
> drive_status_dev_list.  It goes on to instantiate the LED
> and only then adds the device to drive_status_dev_list.
> 
> It's thus possible that the same LED is instantiated twice
> which might confuse users.
>

I missed that, thanks!
Stuart Hayes Oct. 4, 2021, 6:04 p.m. UTC | #4
On 8/18/2021 2:05 AM, Pavel Machek wrote:
> Hi!
> 
>> This patch adds support for the PCIe SSD Status LED Management
>> interface, as described in the "_DSM Additions for PCIe SSD Status LED
>> Management" ECN to the PCI Firmware Specification revision 3.2.
>>
>> It will add (led_classdev) LEDs to each PCIe device that has a supported
>> _DSM interface (one off/on LED per supported state). Both PCIe storage
>> devices, and the ports to which they are connected, can support this
>> interface.
>>
>> Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>
> 
> I believe these are not LEDs, right? This is something that displays
> information to the user, but how exactly it is implemented is up to
> BIOS vendor.
> 
> I don't think it is good fit for LED subsystem.
> 
> Best regards,
> 								Pavel
> 

I think it very likely these will be LEDs on most, if not all, systems 
(likely enough that the PCI ECN has "LEDs" in the name)... they have 
been LEDs on every system I've seen.  I would suspect that systems which 
support more than one or two of the states won't have a 1-to-1 mapping 
of logical LEDs to physical LEDs, though, but might instead use 
something like IBPI (SFF-8489) to use fewer LEDs and be easier to read 
at a glance.

I'm not sure I understand why the LED subsystem would be that much worse 
a fit for this than for keyboard (or other) indicator LEDs.  I 
understand that many other indicators are more likely to have each 
single kernel LED mapped to a single physical LED, but functionally they 
are both kernel LEDs controlling on/off indicators which are likely 
displayed on LEDs that are always visible on the system.
Dan Williams Oct. 5, 2021, 4:41 a.m. UTC | #5
On Mon, 2021-10-04 at 12:41 -0500, stuart hayes wrote:
> 
> 
> On 8/14/2021 1:23 AM, Lukas Wunner wrote:
> > On Fri, Aug 13, 2021 at 05:36:53PM -0400, Stuart Hayes wrote:
> > > +struct mutex drive_status_dev_list_lock;
> > > +struct list_head drive_status_dev_list;
> > 
> > Should be declared static.
> > 
> > > +const guid_t pcie_ssd_leds_dsm_guid =
> > > +       GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
> > > +                 0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
> > 
> > Same.
> > 
> > > +struct drive_status_led_ops dsm_drive_status_led_ops = {
> > > +       .get_supported_states = get_supported_states_dsm,
> > > +       .get_current_states = get_current_states_dsm,
> > > +       .set_current_states = set_current_states_dsm,
> > > +};
> > 
> > Same.
> > 
> 
> Thank you!
> 
> > > +static void probe_pdev(struct pci_dev *pdev)
> > > +{
> > > +       /*
> > > +        * This is only supported on PCIe storage devices and PCIe ports
> > > +        */
> > > +       if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
> > > +           pdev->class != PCI_CLASS_BRIDGE_PCI)
> > > +               return;
> > > +       if (pdev_has_dsm(pdev))
> > > +               add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
> > > +}
> > 
> > Why is &dsm_drive_status_led_ops passed to add_drive_status_dev()?
> > It's always the same argument.
> > 
> 
> Because I hope this will also support NPEM as well, since it is so 
> similar except for using a PCIe extended capability instead of a _DSM
> method. This will make it very easy to add the support... I just don't 
> have any NPEM hardware yet.

I'm interested in helping the infrastructure along so it can be reused
with the CXL memory expander driver. I expect that like most subsystems
PCI does not accept enabling without a user. I.e. the NPEM operations
need to be included to prove out the infrastructure is suitable to
support both LED mechanisms.
Dan Williams Oct. 5, 2021, 5:14 a.m. UTC | #6
On Fri, 2021-08-13 at 17:36 -0400, Stuart Hayes wrote:
> This patch adds support for the PCIe SSD Status LED Management
> interface, as described in the "_DSM Additions for PCIe SSD Status LED
> Management" ECN to the PCI Firmware Specification revision 3.2.
> 
> It will add (led_classdev) LEDs to each PCIe device that has a supported
> _DSM interface (one off/on LED per supported state). Both PCIe storage
> devices, and the ports to which they are connected, can support this
> interface.

Can you describe why this chose the drivers/leds/led-class.c route
instead of drivers/misc/enclosure.c? Or, something simple / open-coded
like drivers/ata/libata-sata.c? If that was already discussed in a
previous posting just point me over there. My initial take away is that
this is spending effort on gear matching with the led_classdev
interface.

The comments that follow are just an initial pass based on being
suprised about the led_classdev choice and the desire for NPEM support.


> 
> Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>
> ---
> V2:
>         * Simplified interface to a single "states" attribute under the LED
>           classdev using only state names
>         * Reworked driver to separate _DSM specific code, so support for
>           NPEM (or other methods) could be easily be added
>         * Use BIT macro
> V3:
>         * Changed code to use a single LED per supported state
>         * Moved to drivers/pci/pcie
>         * Changed Kconfig dependency to LEDS_CLASS instead of NEW_LEDS
>         * Added PCI device class check before _DSM presence check
>         * Other cleanups that don't affect the function
> 
> ---
>  drivers/pci/pcie/Kconfig    |  11 +
>  drivers/pci/pcie/Makefile   |   1 +
>  drivers/pci/pcie/ssd-leds.c | 419 ++++++++++++++++++++++++++++++++++++
>  3 files changed, 431 insertions(+)
>  create mode 100644 drivers/pci/pcie/ssd-leds.c
> 
> diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
> index 45a2ef702b45..b738d473209f 100644
> --- a/drivers/pci/pcie/Kconfig
> +++ b/drivers/pci/pcie/Kconfig
> @@ -142,3 +142,14 @@ config PCIE_EDR
>           the PCI Firmware Specification r3.2.  Enable this if you want to
>           support hybrid DPC model which uses both firmware and OS to
>           implement DPC.
> +
> +config PCIE_SSD_LEDS
> +       tristate "PCIe SSD status LED support"
> +       depends on ACPI && LEDS_CLASS

This "depends on ACPI" is awkward when this grows NPEM support. I feel
like NPEM is the first class citizen and then ACPI optionally overrides
NPEM support if present.


> +       help
> +         Driver for PCIe SSD status LED management as described in a PCI
> +         Firmware Specification, Revision 3.2 ECN.

The auxiliary bus [1] was recently added as a way for drivers to carve
off functionality into sub-device / sub-driver pairs. One benefit from
the auxiliary bus organization is that the NPEM driver gets a propoer
alias and auto-loading support. As is this driver appears to need to be
manually loaded.

[1]: Documentation/driver-api/auxiliary_bus.rst

> +
> +         When enabled, LED interfaces will be created for supported drive
> +         states for each PCIe device that has the ACPI _DSM method described
> +         in the referenced specification.
> diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
> index b2980db88cc0..fbcb8c2d1dc1 100644
> --- a/drivers/pci/pcie/Makefile
> +++ b/drivers/pci/pcie/Makefile
> @@ -13,3 +13,4 @@ obj-$(CONFIG_PCIE_PME)                += pme.o
>  obj-$(CONFIG_PCIE_DPC)         += dpc.o
>  obj-$(CONFIG_PCIE_PTM)         += ptm.o
>  obj-$(CONFIG_PCIE_EDR)         += edr.o
> +obj-$(CONFIG_PCIE_SSD_LEDS)    += ssd-leds.o
> diff --git a/drivers/pci/pcie/ssd-leds.c b/drivers/pci/pcie/ssd-leds.c
> new file mode 100644
> index 000000000000..cacb77e5da2d
> --- /dev/null
> +++ b/drivers/pci/pcie/ssd-leds.c
> @@ -0,0 +1,419 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Module to provide LED interfaces for PCIe SSD status LED states, as
> + * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
> + * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
> + *
> + * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
> + * Management, but uses a _DSM ACPI method rather than a PCIe extended
> + * capability.
> + *
> + * Copyright (c) 2021 Dell Inc.
> + *
> + * TODO: Add NPEM support
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/leds.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/pci.h>
> +#include <uapi/linux/uleds.h>
> +
> +#define DRIVER_NAME    "pcie-ssd-leds"
> +#define DRIVER_VERSION "v1.0"
> +
> +struct led_state {
> +       char *name;
> +       int bit;
> +};
> +
> +static struct led_state led_states[] = {
> +       { .name = "ok",         .bit = 2 },
> +       { .name = "locate",     .bit = 3 },
> +       { .name = "failed",     .bit = 4 },
> +       { .name = "rebuild",    .bit = 5 },
> +       { .name = "pfa",        .bit = 6 },
> +       { .name = "hotspare",   .bit = 7 },
> +       { .name = "ica",        .bit = 8 },
> +       { .name = "ifa",        .bit = 9 },
> +       { .name = "invalid",    .bit = 10 },
> +       { .name = "disabled",   .bit = 11 },
> +};

include/linux/enclosure.h has common ABI definitions of industry
standard enclosure LED settings. The above looks to be open coding the
same?

> +
> +struct drive_status_led_ops {
> +       int (*get_supported_states)(struct pci_dev *pdev, u32 *states);
> +       int (*get_current_states)(struct pci_dev *pdev, u32 *states);
> +       int (*set_current_states)(struct pci_dev *pdev, u32 states);
> +};
> +
> +struct drive_status_state_led {
> +       struct led_classdev cdev;
> +       struct drive_status_dev *dsdev;
> +       int bit;
> +};
> +
> +/*
> + * drive_status_dev->dev could be the drive itself or its PCIe port
> + */
> +struct drive_status_dev {
> +       struct list_head list;
> +       /* PCI device that has the LED controls */
> +       struct pci_dev *pdev;
> +       /* _DSM (or NPEM) LED ops */
> +       struct drive_status_led_ops *ops;
> +       /* currently active states */
> +       u32 states;
> +       int num_leds;
> +       struct drive_status_state_led leds[];
> +};
> +
> +struct mutex drive_status_dev_list_lock;
> +struct list_head drive_status_dev_list;
> +
> +/*
> + * _DSM LED control
> + */
> +const guid_t pcie_ssd_leds_dsm_guid =
> +       GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
> +                 0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
> +
> +#define GET_SUPPORTED_STATES_DSM       0x01
> +#define GET_STATE_DSM                  0x02
> +#define SET_STATE_DSM                  0x03
> +
> +struct ssdleds_dsm_output {
> +       u16 status;
> +       u8 function_specific_err;
> +       u8 vendor_specific_err;
> +       u32 state;
> +};
> +
> +static void dsm_status_err_print(struct pci_dev *pdev,
> +                                struct ssdleds_dsm_output *output)
> +{
> +       switch (output->status) {
> +       case 0:
> +               break;
> +       case 1:
> +               pci_dbg(pdev, "_DSM not supported\n");
> +               break;
> +       case 2:
> +               pci_dbg(pdev, "_DSM invalid input parameters\n");
> +               break;
> +       case 3:
> +               pci_dbg(pdev, "_DSM communication error\n");
> +               break;
> +       case 4:
> +               pci_dbg(pdev, "_DSM function-specific error 0x%x\n",
> +                       output->function_specific_err);
> +               break;
> +       case 5:
> +               pci_dbg(pdev, "_DSM vendor-specific error 0x%x\n",
> +                       output->vendor_specific_err);
> +               break;
> +       default:
> +               pci_dbg(pdev, "_DSM returned unknown status 0x%x\n",
> +                       output->status);
> +       }
> +}
> +
> +static int dsm_set(struct pci_dev *pdev, u32 value)
> +{
> +       acpi_handle handle;
> +       union acpi_object *out_obj, arg3[2];
> +       struct ssdleds_dsm_output *dsm_output;
> +
> +       handle = ACPI_HANDLE(&pdev->dev);
> +       if (!handle)
> +               return -ENODEV;
> +
> +       arg3[0].type = ACPI_TYPE_PACKAGE;
> +       arg3[0].package.count = 1;
> +       arg3[0].package.elements = &arg3[1];
> +
> +       arg3[1].type = ACPI_TYPE_BUFFER;
> +       arg3[1].buffer.length = 4;
> +       arg3[1].buffer.pointer = (u8 *)&value;
> +
> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
> +                               1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
> +       if (!out_obj)
> +               return -EIO;
> +
> +       if (out_obj->buffer.length < 8) {
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +
> +       if (dsm_output->status != 0) {
> +               dsm_status_err_print(pdev, dsm_output);
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +       ACPI_FREE(out_obj);
> +       return 0;
> +}
> +
> +static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
> +{
> +       acpi_handle handle;
> +       union acpi_object *out_obj;
> +       struct ssdleds_dsm_output *dsm_output;
> +
> +       handle = ACPI_HANDLE(&pdev->dev);
> +       if (!handle)
> +               return -ENODEV;
> +
> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +                                         dsm_func, NULL, ACPI_TYPE_BUFFER);
> +       if (!out_obj)
> +               return -EIO;
> +
> +       if (out_obj->buffer.length < 8) {
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +       if (dsm_output->status != 0) {
> +               dsm_status_err_print(pdev, dsm_output);
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       *output = dsm_output->state;
> +       ACPI_FREE(out_obj);
> +       return 0;
> +}
> +
> +static int get_supported_states_dsm(struct pci_dev *pdev, u32 *states)
> +{
> +       return dsm_get(pdev, GET_SUPPORTED_STATES_DSM, states);
> +}
> +
> +static int get_current_states_dsm(struct pci_dev *pdev, u32 *states)
> +{
> +       return dsm_get(pdev, GET_STATE_DSM, states);
> +}
> +
> +static int set_current_states_dsm(struct pci_dev *pdev, u32 states)
> +{
> +       return dsm_set(pdev, states);
> +}
> +
> +static bool pdev_has_dsm(struct pci_dev *pdev)
> +{
> +       acpi_handle handle;
> +
> +       handle = ACPI_HANDLE(&pdev->dev);
> +       if (!handle)
> +               return false;
> +
> +       return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +                             1 << GET_SUPPORTED_STATES_DSM ||
> +                             1 << GET_STATE_DSM ||
> +                             1 << SET_STATE_DSM);
> +}
> +
> +struct drive_status_led_ops dsm_drive_status_led_ops = {
> +       .get_supported_states = get_supported_states_dsm,
> +       .get_current_states = get_current_states_dsm,
> +       .set_current_states = set_current_states_dsm,
> +};
> +
> +/*
> + * code not specific to method (_DSM/NPEM)
> + */
> +
> +static int set_brightness(struct led_classdev *led_cdev,
> +                                      enum led_brightness brightness)
> +{
> +       struct drive_status_state_led *led;
> +       int err;
> +
> +       led = container_of(led_cdev, struct drive_status_state_led, cdev);
> +
> +       if (brightness == LED_OFF)
> +               clear_bit(led->bit, (unsigned long *)&(led->dsdev->states));
> +       else
> +               set_bit(led->bit, (unsigned long *)&(led->dsdev->states));
> +       err = led->dsdev->ops->set_current_states(led->dsdev->pdev,
> +                                                 led->dsdev->states);
> +       if (err < 0)
> +               return err;
> +       return 0;
> +}
> +
> +static enum led_brightness get_brightness(struct led_classdev *led_cdev)
> +{
> +       struct drive_status_state_led *led;
> +
> +       led = container_of(led_cdev, struct drive_status_state_led, cdev);
> +       return test_bit(led->bit, (unsigned long *)&led->dsdev->states)
> +               ? LED_ON : LED_OFF;
> +}
> +
> +static struct drive_status_dev *to_drive_status_dev(struct pci_dev *pdev)
> +{
> +       struct drive_status_dev *dsdev;
> +
> +       mutex_lock(&drive_status_dev_list_lock);
> +       list_for_each_entry(dsdev, &drive_status_dev_list, list)
> +               if (pdev == dsdev->pdev) {
> +                       mutex_unlock(&drive_status_dev_list_lock);
> +                       return dsdev;
> +               }
> +       mutex_unlock(&drive_status_dev_list_lock);
> +       return NULL;
> +}
> +
> +static void remove_drive_status_dev(struct drive_status_dev *dsdev)
> +{
> +       if (dsdev) {
> +               int i;
> +
> +               mutex_lock(&drive_status_dev_list_lock);
> +               list_del(&dsdev->list);
> +               mutex_unlock(&drive_status_dev_list_lock);
> +               for (i = 0; i < dsdev->num_leds; i++)
> +                       led_classdev_unregister(&dsdev->leds[i].cdev);
> +               kfree(dsdev);
> +       }
> +}
> +
> +static void add_drive_status_dev(struct pci_dev *pdev,
> +                                struct drive_status_led_ops *ops)
> +{
> +       u32 supported;
> +       int ret, num_leds, i;
> +       struct drive_status_dev *dsdev;
> +       char name[LED_MAX_NAME_SIZE];
> +       struct drive_status_state_led *led;
> +
> +       if (to_drive_status_dev(pdev))
> +               /*
> +                * leds have already been added for this dev
> +                */
> +               return;
> +
> +       if (ops->get_supported_states(pdev, &supported) < 0)
> +               return;
> +       num_leds = hweight32(supported);
> +       if (num_leds == 0)
> +               return;
> +
> +       dsdev = kzalloc(struct_size(dsdev, leds, num_leds), GFP_KERNEL);
> +       if (!dsdev)
> +               return;
> +
> +       dsdev->num_leds = 0;
> +       dsdev->pdev = pdev;
> +       dsdev->ops = ops;
> +       dsdev->states = 0;
> +       if (ops->set_current_states(pdev, dsdev->states)) {
> +               kfree(dsdev);
> +               return;
> +       }
> +       INIT_LIST_HEAD(&dsdev->list);
> +       /*
> +        * add LEDs only for supported states
> +        */
> +       for (i = 0; i < ARRAY_SIZE(led_states); i++) {
> +               if (!test_bit(led_states[i].bit, (unsigned long *)&supported))
> +                       continue;
> +
> +               led = &dsdev->leds[dsdev->num_leds];
> +               led->dsdev = dsdev;
> +               led->bit = led_states[i].bit;
> +
> +               snprintf(name, sizeof(name), "%s::%s",
> +                        pci_name(pdev), led_states[i].name);
> +               led->cdev.name = name;
> +               led->cdev.max_brightness = LED_ON;
> +               led->cdev.brightness_set_blocking = set_brightness;
> +               led->cdev.brightness_get = get_brightness;
> +               ret = 0;
> +               ret = led_classdev_register(&pdev->dev, &led->cdev);
> +               if (ret) {
> +                       pr_warn("Failed to register LEDs for %s\n", pci_name(pdev));
> +                       remove_drive_status_dev(dsdev);
> +                       return;
> +               }
> +               dsdev->num_leds++;
> +       }
> +
> +       mutex_lock(&drive_status_dev_list_lock);
> +       list_add_tail(&dsdev->list, &drive_status_dev_list);
> +       mutex_unlock(&drive_status_dev_list_lock);
> +}
> +
> +/*
> + * code specific to PCIe devices
> + */
> +static void probe_pdev(struct pci_dev *pdev)
> +{
> +       /*
> +        * This is only supported on PCIe storage devices and PCIe ports
> +        */
> +       if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
> +           pdev->class != PCI_CLASS_BRIDGE_PCI)
> +               return;
> +       if (pdev_has_dsm(pdev))
> +               add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
> +}
> +
> +static int ssd_leds_pci_bus_notifier_cb(struct notifier_block *nb,
> +                                          unsigned long action, void *data)
> +{
> +       struct pci_dev *pdev = to_pci_dev(data);
> +
> +       if (action == BUS_NOTIFY_ADD_DEVICE)
> +               probe_pdev(pdev);
> +       else if (action == BUS_NOTIFY_DEL_DEVICE)
> +               remove_drive_status_dev(to_drive_status_dev(pdev));
> +       return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block ssd_leds_pci_bus_nb = {
> +       .notifier_call = ssd_leds_pci_bus_notifier_cb,
> +       .priority = INT_MIN,
> +};
> +
> +static void initial_scan_for_leds(void)
> +{
> +       struct pci_dev *pdev = NULL;
> +
> +       for_each_pci_dev(pdev)
> +               probe_pdev(pdev);


This looks odd to me. Why force enable every occurrence these leds, and
do so indepdendently of the bind state of the driver for the associated
PCI device? I would expect that this support would be a library called
by the NVME driver, or the CXL driver. A library just like the
led_classdev infrastructure.


> +}
> +
> +static int __init ssd_leds_init(void)
> +{
> +       mutex_init(&drive_status_dev_list_lock);
> +       INIT_LIST_HEAD(&drive_status_dev_list);
> +
> +       bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
> +       initial_scan_for_leds();
> +       return 0;
> +}
> +
> +static void __exit ssd_leds_exit(void)
> +{
> +       struct drive_status_dev *dsdev, *temp;
> +
> +       bus_unregister_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
> +       list_for_each_entry_safe(dsdev, temp, &drive_status_dev_list, list)
> +               remove_drive_status_dev(dsdev);
> +}
> +
> +module_init(ssd_leds_init);
> +module_exit(ssd_leds_exit);
> +
> +MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
> +MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
> +MODULE_LICENSE("GPL");
Stuart Hayes Oct. 6, 2021, 2:42 a.m. UTC | #7
On 10/5/2021 12:14 AM, Williams, Dan J wrote:
> On Fri, 2021-08-13 at 17:36 -0400, Stuart Hayes wrote:
>> This patch adds support for the PCIe SSD Status LED Management
>> interface, as described in the "_DSM Additions for PCIe SSD Status LED
>> Management" ECN to the PCI Firmware Specification revision 3.2.
>>
>> It will add (led_classdev) LEDs to each PCIe device that has a supported
>> _DSM interface (one off/on LED per supported state). Both PCIe storage
>> devices, and the ports to which they are connected, can support this
>> interface.
> 
> Can you describe why this chose the drivers/leds/led-class.c route
> instead of drivers/misc/enclosure.c? Or, something simple / open-coded
> like drivers/ata/libata-sata.c? If that was already discussed in a
> previous posting just point me over there. My initial take away is that
> this is spending effort on gear matching with the led_classdev
> interface.
> 
> The comments that follow are just an initial pass based on being
> suprised about the led_classdev choice and the desire for NPEM support.
> 
> 

Thank you for the comments, Dan.

I originally did submit something simple that just added a couple of 
sysfs attributes to allow userspace access to the _DSM, but Greg K-H 
said (1) that I shouldn't create new driver-specific sysfs files that do 
things that existing class drivers do, and that if I'm allowing LEDs to 
be controlled by the user, I have to use the LED subsystem, so I went 
with that. (See the end of 
https://patchwork.ozlabs.org/project/linux-pci/patch/20201110153735.58587-1-stuart.w.hayes@gmail.com/)

I tried to make my code very similar to drivers/input/input-leds.c, 
where up to 11 LEDs per input device are registered.  Since NPEM/_DSM 
allows any of the states to be independently set or cleared, and any of 
the states may or may not be supported, it seemed very similar to me 
(except, as Pavel points out, the NPEM/_DSM states probably won't have 
one LED per state like the keyboard LEDs typically do).

I will look into drivers/misc/enclosure.c.  I think I didn't go that 
route initially as it appeared to be quite old and not widely used, and 
the states don't completely overlap.  More about it below.

>>
>> Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>
>> ---
>> V2:
>>          * Simplified interface to a single "states" attribute under the LED
>>            classdev using only state names
>>          * Reworked driver to separate _DSM specific code, so support for
>>            NPEM (or other methods) could be easily be added
>>          * Use BIT macro
>> V3:
>>          * Changed code to use a single LED per supported state
>>          * Moved to drivers/pci/pcie
>>          * Changed Kconfig dependency to LEDS_CLASS instead of NEW_LEDS
>>          * Added PCI device class check before _DSM presence check
>>          * Other cleanups that don't affect the function
>>
>> ---
>>   drivers/pci/pcie/Kconfig    |  11 +
>>   drivers/pci/pcie/Makefile   |   1 +
>>   drivers/pci/pcie/ssd-leds.c | 419 ++++++++++++++++++++++++++++++++++++
>>   3 files changed, 431 insertions(+)
>>   create mode 100644 drivers/pci/pcie/ssd-leds.c
>>
>> diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
>> index 45a2ef702b45..b738d473209f 100644
>> --- a/drivers/pci/pcie/Kconfig
>> +++ b/drivers/pci/pcie/Kconfig
>> @@ -142,3 +142,14 @@ config PCIE_EDR
>>            the PCI Firmware Specification r3.2.  Enable this if you want to
>>            support hybrid DPC model which uses both firmware and OS to
>>            implement DPC.
>> +
>> +config PCIE_SSD_LEDS
>> +       tristate "PCIe SSD status LED support"
>> +       depends on ACPI && LEDS_CLASS
> 
> This "depends on ACPI" is awkward when this grows NPEM support. I feel
> like NPEM is the first class citizen and then ACPI optionally overrides
> NPEM support if present.
> 
> 

I didn't actually think about NPEM when I originally submitted this... 
and even now, as I submitted it, it only uses the _DSM method, so it is 
useless without ACPI.  The ACPI dependency could go if NPEM support was 
added, for sure.

>> +       help
>> +         Driver for PCIe SSD status LED management as described in a PCI
>> +         Firmware Specification, Revision 3.2 ECN.
> 
> The auxiliary bus [1] was recently added as a way for drivers to carve
> off functionality into sub-device / sub-driver pairs. One benefit from
> the auxiliary bus organization is that the NPEM driver gets a propoer
> alias and auto-loading support. As is this driver appears to need to be
> manually loaded.
> 
> [1]: Documentation/driver-api/auxiliary_bus.rst
> 

Yes, unfortunately, it would need to be manually loaded.  I flip-flopped 
multiple times on making this just a library that the nvme driver could 
call to register the LEDs (if the _DSM was present).  It would be 
simpler, it wouldn't need a module to be manulaly loaded, it would 
resulted in better LED names (if the _DSM was on the NVMe PCI device), 
and it would mean that the driver that owns the PCI device would also 
own the LEDs.

The only reason I didn't do that, is that the _DSM can be on the PCIe 
port that the NVMe device is connected to, rather than on the NVMe drive 
PCI device.  And I'm not sure how to deal with the latter situation, 
except maybe change the pcie port bus driver to also call in to the 
library to register LEDs somehow.  But maybe it would be worth trying to 
figure that out.

I was not aware of the auxiliary bus, thanks for pointing it out.  I 
glanced over it, and I'm not sure how it would help over just having a 
library that nvme, pcie port bus driver, CXL, or whatever could call into.


>> +
>> +         When enabled, LED interfaces will be created for supported drive
>> +         states for each PCIe device that has the ACPI _DSM method described
>> +         in the referenced specification.
>> diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
>> index b2980db88cc0..fbcb8c2d1dc1 100644
>> --- a/drivers/pci/pcie/Makefile
>> +++ b/drivers/pci/pcie/Makefile
>> @@ -13,3 +13,4 @@ obj-$(CONFIG_PCIE_PME)                += pme.o
>>   obj-$(CONFIG_PCIE_DPC)         += dpc.o
>>   obj-$(CONFIG_PCIE_PTM)         += ptm.o
>>   obj-$(CONFIG_PCIE_EDR)         += edr.o
>> +obj-$(CONFIG_PCIE_SSD_LEDS)    += ssd-leds.o
>> diff --git a/drivers/pci/pcie/ssd-leds.c b/drivers/pci/pcie/ssd-leds.c
>> new file mode 100644
>> index 000000000000..cacb77e5da2d
>> --- /dev/null
>> +++ b/drivers/pci/pcie/ssd-leds.c
>> @@ -0,0 +1,419 @@
>> +// SPDX-License-Identifier: GPL-2.0-only
>> +/*
>> + * Module to provide LED interfaces for PCIe SSD status LED states, as
>> + * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
>> + * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
>> + *
>> + * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
>> + * Management, but uses a _DSM ACPI method rather than a PCIe extended
>> + * capability.
>> + *
>> + * Copyright (c) 2021 Dell Inc.
>> + *
>> + * TODO: Add NPEM support
>> + */
>> +
>> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
>> +
>> +#include <linux/acpi.h>
>> +#include <linux/leds.h>
>> +#include <linux/module.h>
>> +#include <linux/mutex.h>
>> +#include <linux/pci.h>
>> +#include <uapi/linux/uleds.h>
>> +
>> +#define DRIVER_NAME    "pcie-ssd-leds"
>> +#define DRIVER_VERSION "v1.0"
>> +
>> +struct led_state {
>> +       char *name;
>> +       int bit;
>> +};
>> +
>> +static struct led_state led_states[] = {
>> +       { .name = "ok",         .bit = 2 },
>> +       { .name = "locate",     .bit = 3 },
>> +       { .name = "failed",     .bit = 4 },
>> +       { .name = "rebuild",    .bit = 5 },
>> +       { .name = "pfa",        .bit = 6 },
>> +       { .name = "hotspare",   .bit = 7 },
>> +       { .name = "ica",        .bit = 8 },
>> +       { .name = "ifa",        .bit = 9 },
>> +       { .name = "invalid",    .bit = 10 },
>> +       { .name = "disabled",   .bit = 11 },
>> +};
> 
> include/linux/enclosure.h has common ABI definitions of industry
> standard enclosure LED settings. The above looks to be open coding the
> same?
>

The LED states in inluce/linux/enclosure.h aren't exactly the same... 
there are states defined in NPEM/_DSM that aren't defined in 
enclosure.h.  In addition, while the enclosure driver allows "locate" to 
be controlled independently, it looks like it will only allow a single 
state (unsupported/ok/critical/etc) to be active at a time, while the 
NPEM/_DSM allow all of the state bits to be independently set or 
cleared.  Maybe only one of those states would need to be set at a time, 
I don't know, but that would impose a limitation on what NPEM/_DSM can 
do.  I'll take a closer look at this as an alternative to using 
drivers/leds/led-class.c.


>> +
>> +struct drive_status_led_ops {
>> +       int (*get_supported_states)(struct pci_dev *pdev, u32 *states);
>> +       int (*get_current_states)(struct pci_dev *pdev, u32 *states);
>> +       int (*set_current_states)(struct pci_dev *pdev, u32 states);
>> +};
>> +
>> +struct drive_status_state_led {
>> +       struct led_classdev cdev;
>> +       struct drive_status_dev *dsdev;
>> +       int bit;
>> +};
>> +
>> +/*
>> + * drive_status_dev->dev could be the drive itself or its PCIe port
>> + */
>> +struct drive_status_dev {
>> +       struct list_head list;
>> +       /* PCI device that has the LED controls */
>> +       struct pci_dev *pdev;
>> +       /* _DSM (or NPEM) LED ops */
>> +       struct drive_status_led_ops *ops;
>> +       /* currently active states */
>> +       u32 states;
>> +       int num_leds;
>> +       struct drive_status_state_led leds[];
>> +};
>> +
>> +struct mutex drive_status_dev_list_lock;
>> +struct list_head drive_status_dev_list;
>> +
>> +/*
>> + * _DSM LED control
>> + */
>> +const guid_t pcie_ssd_leds_dsm_guid =
>> +       GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
>> +                 0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
>> +
>> +#define GET_SUPPORTED_STATES_DSM       0x01
>> +#define GET_STATE_DSM                  0x02
>> +#define SET_STATE_DSM                  0x03
>> +
>> +struct ssdleds_dsm_output {
>> +       u16 status;
>> +       u8 function_specific_err;
>> +       u8 vendor_specific_err;
>> +       u32 state;
>> +};
>> +
>> +static void dsm_status_err_print(struct pci_dev *pdev,
>> +                                struct ssdleds_dsm_output *output)
>> +{
>> +       switch (output->status) {
>> +       case 0:
>> +               break;
>> +       case 1:
>> +               pci_dbg(pdev, "_DSM not supported\n");
>> +               break;
>> +       case 2:
>> +               pci_dbg(pdev, "_DSM invalid input parameters\n");
>> +               break;
>> +       case 3:
>> +               pci_dbg(pdev, "_DSM communication error\n");
>> +               break;
>> +       case 4:
>> +               pci_dbg(pdev, "_DSM function-specific error 0x%x\n",
>> +                       output->function_specific_err);
>> +               break;
>> +       case 5:
>> +               pci_dbg(pdev, "_DSM vendor-specific error 0x%x\n",
>> +                       output->vendor_specific_err);
>> +               break;
>> +       default:
>> +               pci_dbg(pdev, "_DSM returned unknown status 0x%x\n",
>> +                       output->status);
>> +       }
>> +}
>> +
>> +static int dsm_set(struct pci_dev *pdev, u32 value)
>> +{
>> +       acpi_handle handle;
>> +       union acpi_object *out_obj, arg3[2];
>> +       struct ssdleds_dsm_output *dsm_output;
>> +
>> +       handle = ACPI_HANDLE(&pdev->dev);
>> +       if (!handle)
>> +               return -ENODEV;
>> +
>> +       arg3[0].type = ACPI_TYPE_PACKAGE;
>> +       arg3[0].package.count = 1;
>> +       arg3[0].package.elements = &arg3[1];
>> +
>> +       arg3[1].type = ACPI_TYPE_BUFFER;
>> +       arg3[1].buffer.length = 4;
>> +       arg3[1].buffer.pointer = (u8 *)&value;
>> +
>> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
>> +                               1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
>> +       if (!out_obj)
>> +               return -EIO;
>> +
>> +       if (out_obj->buffer.length < 8) {
>> +               ACPI_FREE(out_obj);
>> +               return -EIO;
>> +       }
>> +
>> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
>> +
>> +       if (dsm_output->status != 0) {
>> +               dsm_status_err_print(pdev, dsm_output);
>> +               ACPI_FREE(out_obj);
>> +               return -EIO;
>> +       }
>> +       ACPI_FREE(out_obj);
>> +       return 0;
>> +}
>> +
>> +static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
>> +{
>> +       acpi_handle handle;
>> +       union acpi_object *out_obj;
>> +       struct ssdleds_dsm_output *dsm_output;
>> +
>> +       handle = ACPI_HANDLE(&pdev->dev);
>> +       if (!handle)
>> +               return -ENODEV;
>> +
>> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
>> +                                         dsm_func, NULL, ACPI_TYPE_BUFFER);
>> +       if (!out_obj)
>> +               return -EIO;
>> +
>> +       if (out_obj->buffer.length < 8) {
>> +               ACPI_FREE(out_obj);
>> +               return -EIO;
>> +       }
>> +
>> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
>> +       if (dsm_output->status != 0) {
>> +               dsm_status_err_print(pdev, dsm_output);
>> +               ACPI_FREE(out_obj);
>> +               return -EIO;
>> +       }
>> +
>> +       *output = dsm_output->state;
>> +       ACPI_FREE(out_obj);
>> +       return 0;
>> +}
>> +
>> +static int get_supported_states_dsm(struct pci_dev *pdev, u32 *states)
>> +{
>> +       return dsm_get(pdev, GET_SUPPORTED_STATES_DSM, states);
>> +}
>> +
>> +static int get_current_states_dsm(struct pci_dev *pdev, u32 *states)
>> +{
>> +       return dsm_get(pdev, GET_STATE_DSM, states);
>> +}
>> +
>> +static int set_current_states_dsm(struct pci_dev *pdev, u32 states)
>> +{
>> +       return dsm_set(pdev, states);
>> +}
>> +
>> +static bool pdev_has_dsm(struct pci_dev *pdev)
>> +{
>> +       acpi_handle handle;
>> +
>> +       handle = ACPI_HANDLE(&pdev->dev);
>> +       if (!handle)
>> +               return false;
>> +
>> +       return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
>> +                             1 << GET_SUPPORTED_STATES_DSM ||
>> +                             1 << GET_STATE_DSM ||
>> +                             1 << SET_STATE_DSM);
>> +}
>> +
>> +struct drive_status_led_ops dsm_drive_status_led_ops = {
>> +       .get_supported_states = get_supported_states_dsm,
>> +       .get_current_states = get_current_states_dsm,
>> +       .set_current_states = set_current_states_dsm,
>> +};
>> +
>> +/*
>> + * code not specific to method (_DSM/NPEM)
>> + */
>> +
>> +static int set_brightness(struct led_classdev *led_cdev,
>> +                                      enum led_brightness brightness)
>> +{
>> +       struct drive_status_state_led *led;
>> +       int err;
>> +
>> +       led = container_of(led_cdev, struct drive_status_state_led, cdev);
>> +
>> +       if (brightness == LED_OFF)
>> +               clear_bit(led->bit, (unsigned long *)&(led->dsdev->states));
>> +       else
>> +               set_bit(led->bit, (unsigned long *)&(led->dsdev->states));
>> +       err = led->dsdev->ops->set_current_states(led->dsdev->pdev,
>> +                                                 led->dsdev->states);
>> +       if (err < 0)
>> +               return err;
>> +       return 0;
>> +}
>> +
>> +static enum led_brightness get_brightness(struct led_classdev *led_cdev)
>> +{
>> +       struct drive_status_state_led *led;
>> +
>> +       led = container_of(led_cdev, struct drive_status_state_led, cdev);
>> +       return test_bit(led->bit, (unsigned long *)&led->dsdev->states)
>> +               ? LED_ON : LED_OFF;
>> +}
>> +
>> +static struct drive_status_dev *to_drive_status_dev(struct pci_dev *pdev)
>> +{
>> +       struct drive_status_dev *dsdev;
>> +
>> +       mutex_lock(&drive_status_dev_list_lock);
>> +       list_for_each_entry(dsdev, &drive_status_dev_list, list)
>> +               if (pdev == dsdev->pdev) {
>> +                       mutex_unlock(&drive_status_dev_list_lock);
>> +                       return dsdev;
>> +               }
>> +       mutex_unlock(&drive_status_dev_list_lock);
>> +       return NULL;
>> +}
>> +
>> +static void remove_drive_status_dev(struct drive_status_dev *dsdev)
>> +{
>> +       if (dsdev) {
>> +               int i;
>> +
>> +               mutex_lock(&drive_status_dev_list_lock);
>> +               list_del(&dsdev->list);
>> +               mutex_unlock(&drive_status_dev_list_lock);
>> +               for (i = 0; i < dsdev->num_leds; i++)
>> +                       led_classdev_unregister(&dsdev->leds[i].cdev);
>> +               kfree(dsdev);
>> +       }
>> +}
>> +
>> +static void add_drive_status_dev(struct pci_dev *pdev,
>> +                                struct drive_status_led_ops *ops)
>> +{
>> +       u32 supported;
>> +       int ret, num_leds, i;
>> +       struct drive_status_dev *dsdev;
>> +       char name[LED_MAX_NAME_SIZE];
>> +       struct drive_status_state_led *led;
>> +
>> +       if (to_drive_status_dev(pdev))
>> +               /*
>> +                * leds have already been added for this dev
>> +                */
>> +               return;
>> +
>> +       if (ops->get_supported_states(pdev, &supported) < 0)
>> +               return;
>> +       num_leds = hweight32(supported);
>> +       if (num_leds == 0)
>> +               return;
>> +
>> +       dsdev = kzalloc(struct_size(dsdev, leds, num_leds), GFP_KERNEL);
>> +       if (!dsdev)
>> +               return;
>> +
>> +       dsdev->num_leds = 0;
>> +       dsdev->pdev = pdev;
>> +       dsdev->ops = ops;
>> +       dsdev->states = 0;
>> +       if (ops->set_current_states(pdev, dsdev->states)) {
>> +               kfree(dsdev);
>> +               return;
>> +       }
>> +       INIT_LIST_HEAD(&dsdev->list);
>> +       /*
>> +        * add LEDs only for supported states
>> +        */
>> +       for (i = 0; i < ARRAY_SIZE(led_states); i++) {
>> +               if (!test_bit(led_states[i].bit, (unsigned long *)&supported))
>> +                       continue;
>> +
>> +               led = &dsdev->leds[dsdev->num_leds];
>> +               led->dsdev = dsdev;
>> +               led->bit = led_states[i].bit;
>> +
>> +               snprintf(name, sizeof(name), "%s::%s",
>> +                        pci_name(pdev), led_states[i].name);
>> +               led->cdev.name = name;
>> +               led->cdev.max_brightness = LED_ON;
>> +               led->cdev.brightness_set_blocking = set_brightness;
>> +               led->cdev.brightness_get = get_brightness;
>> +               ret = 0;
>> +               ret = led_classdev_register(&pdev->dev, &led->cdev);
>> +               if (ret) {
>> +                       pr_warn("Failed to register LEDs for %s\n", pci_name(pdev));
>> +                       remove_drive_status_dev(dsdev);
>> +                       return;
>> +               }
>> +               dsdev->num_leds++;
>> +       }
>> +
>> +       mutex_lock(&drive_status_dev_list_lock);
>> +       list_add_tail(&dsdev->list, &drive_status_dev_list);
>> +       mutex_unlock(&drive_status_dev_list_lock);
>> +}
>> +
>> +/*
>> + * code specific to PCIe devices
>> + */
>> +static void probe_pdev(struct pci_dev *pdev)
>> +{
>> +       /*
>> +        * This is only supported on PCIe storage devices and PCIe ports
>> +        */
>> +       if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
>> +           pdev->class != PCI_CLASS_BRIDGE_PCI)
>> +               return;
>> +       if (pdev_has_dsm(pdev))
>> +               add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
>> +}
>> +
>> +static int ssd_leds_pci_bus_notifier_cb(struct notifier_block *nb,
>> +                                          unsigned long action, void *data)
>> +{
>> +       struct pci_dev *pdev = to_pci_dev(data);
>> +
>> +       if (action == BUS_NOTIFY_ADD_DEVICE)
>> +               probe_pdev(pdev);
>> +       else if (action == BUS_NOTIFY_DEL_DEVICE)
>> +               remove_drive_status_dev(to_drive_status_dev(pdev));
>> +       return NOTIFY_DONE;
>> +}
>> +
>> +static struct notifier_block ssd_leds_pci_bus_nb = {
>> +       .notifier_call = ssd_leds_pci_bus_notifier_cb,
>> +       .priority = INT_MIN,
>> +};
>> +
>> +static void initial_scan_for_leds(void)
>> +{
>> +       struct pci_dev *pdev = NULL;
>> +
>> +       for_each_pci_dev(pdev)
>> +               probe_pdev(pdev);
> 
> 
> This looks odd to me. Why force enable every occurrence these leds, and
> do so indepdendently of the bind state of the driver for the associated
> PCI device? I would expect that this support would be a library called
> by the NVME driver, or the CXL driver. A library just like the
> led_classdev infrastructure.
> 

I guess I didn't know of any reason why they shouldn't be enabled even 
if the nvme driver wasn't bound, if this driver was stand-alone, since 
the LED function doesn't depend on the nvme driver.  But yes, maybe it 
would be better to make this a library that the nvme driver (or pcie 
port bus driver, or CXL driver) calls to register LEDs.

Thank you for taking a look at this... I'm very glad for any help to get 
this done.

> 
>> +}
>> +
>> +static int __init ssd_leds_init(void)
>> +{
>> +       mutex_init(&drive_status_dev_list_lock);
>> +       INIT_LIST_HEAD(&drive_status_dev_list);
>> +
>> +       bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
>> +       initial_scan_for_leds();
>> +       return 0;
>> +}
>> +
>> +static void __exit ssd_leds_exit(void)
>> +{
>> +       struct drive_status_dev *dsdev, *temp;
>> +
>> +       bus_unregister_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
>> +       list_for_each_entry_safe(dsdev, temp, &drive_status_dev_list, list)
>> +               remove_drive_status_dev(dsdev);
>> +}
>> +
>> +module_init(ssd_leds_init);
>> +module_exit(ssd_leds_exit);
>> +
>> +MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
>> +MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
>> +MODULE_LICENSE("GPL");
>
Dan Williams Oct. 6, 2021, 8:15 p.m. UTC | #8
[ add Mariusz from ledmon tool: https://github.com/intel/ledmon ]

On Tue, Oct 5, 2021 at 7:43 PM stuart hayes <stuart.w.hayes@gmail.com> wrote:
>
>
> On 10/5/2021 12:14 AM, Williams, Dan J wrote:
> > On Fri, 2021-08-13 at 17:36 -0400, Stuart Hayes wrote:
> >> This patch adds support for the PCIe SSD Status LED Management
> >> interface, as described in the "_DSM Additions for PCIe SSD Status LED
> >> Management" ECN to the PCI Firmware Specification revision 3.2.
> >>
> >> It will add (led_classdev) LEDs to each PCIe device that has a supported
> >> _DSM interface (one off/on LED per supported state). Both PCIe storage
> >> devices, and the ports to which they are connected, can support this
> >> interface.
> >
> > Can you describe why this chose the drivers/leds/led-class.c route
> > instead of drivers/misc/enclosure.c? Or, something simple / open-coded
> > like drivers/ata/libata-sata.c? If that was already discussed in a
> > previous posting just point me over there. My initial take away is that
> > this is spending effort on gear matching with the led_classdev
> > interface.
> >
> > The comments that follow are just an initial pass based on being
> > suprised about the led_classdev choice and the desire for NPEM support.
> >
> >
>
> Thank you for the comments, Dan.
>
> I originally did submit something simple that just added a couple of
> sysfs attributes to allow userspace access to the _DSM, but Greg K-H
> said (1) that I shouldn't create new driver-specific sysfs files that do
> things that existing class drivers do, and that if I'm allowing LEDs to
> be controlled by the user, I have to use the LED subsystem, so I went
> with that. (See the end of
> https://patchwork.ozlabs.org/project/linux-pci/patch/20201110153735.58587-1-stuart.w.hayes@gmail.com/)

I agree with the general sentiment to adopt and extend existing ABIs
wherever possible, it's just not clear to me that the LED class driver
is the best fit. The Enclosure class infrastructure, in addition to
having some concept of industry standard LED signalling patterns, also
has the concept of linking back to the controller of the storage
device in question. So my gut feel is that it's a better starting
point. If it has some gaps or mismatches with NPEM concepts then
that's a good reason to extend it versus trying to describe the
storage use case to the generic LED class driver.

> I tried to make my code very similar to drivers/input/input-leds.c,
> where up to 11 LEDs per input device are registered.  Since NPEM/_DSM
> allows any of the states to be independently set or cleared, and any of
> the states may or may not be supported, it seemed very similar to me
> (except, as Pavel points out, the NPEM/_DSM states probably won't have
> one LED per state like the keyboard LEDs typically do).
>
> I will look into drivers/misc/enclosure.c.  I think I didn't go that
> route initially as it appeared to be quite old and not widely used, and
> the states don't completely overlap.  More about it below.

The SCSI SES driver takes full advantage of the ENCLOSURE driver-core,
so I suspect layering NPEM / SSD_LED_DSM on top of ENCLOSURE is a
better fit than layering those same concepts on top of LED. As for
usages beyond SES you can see that ledmon avoided the kernel
altogether for its support via direct userspace configuration cycles,
but that's incompatible with ACPI DSMs that need a kernel driver, and
its incompatible with LOCKDOWN_KERNEL where root is disallowed from
issuing configuration write cycles.

>
> >>
> >> Signed-off-by: Stuart Hayes <stuart.w.hayes@gmail.com>
> >> ---
> >> V2:
> >>          * Simplified interface to a single "states" attribute under the LED
> >>            classdev using only state names
> >>          * Reworked driver to separate _DSM specific code, so support for
> >>            NPEM (or other methods) could be easily be added
> >>          * Use BIT macro
> >> V3:
> >>          * Changed code to use a single LED per supported state
> >>          * Moved to drivers/pci/pcie
> >>          * Changed Kconfig dependency to LEDS_CLASS instead of NEW_LEDS
> >>          * Added PCI device class check before _DSM presence check
> >>          * Other cleanups that don't affect the function
> >>
> >> ---
> >>   drivers/pci/pcie/Kconfig    |  11 +
> >>   drivers/pci/pcie/Makefile   |   1 +
> >>   drivers/pci/pcie/ssd-leds.c | 419 ++++++++++++++++++++++++++++++++++++
> >>   3 files changed, 431 insertions(+)
> >>   create mode 100644 drivers/pci/pcie/ssd-leds.c
> >>
> >> diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
> >> index 45a2ef702b45..b738d473209f 100644
> >> --- a/drivers/pci/pcie/Kconfig
> >> +++ b/drivers/pci/pcie/Kconfig
> >> @@ -142,3 +142,14 @@ config PCIE_EDR
> >>            the PCI Firmware Specification r3.2.  Enable this if you want to
> >>            support hybrid DPC model which uses both firmware and OS to
> >>            implement DPC.
> >> +
> >> +config PCIE_SSD_LEDS
> >> +       tristate "PCIe SSD status LED support"
> >> +       depends on ACPI && LEDS_CLASS
> >
> > This "depends on ACPI" is awkward when this grows NPEM support. I feel
> > like NPEM is the first class citizen and then ACPI optionally overrides
> > NPEM support if present.
> >
> >
>
> I didn't actually think about NPEM when I originally submitted this...
> and even now, as I submitted it, it only uses the _DSM method, so it is
> useless without ACPI.  The ACPI dependency could go if NPEM support was
> added, for sure.

As far as I can tell from the specification NPEM is the discovery
mechanism for which PCI device to perform the _DSM. Maybe someone who
is more familiar with the intent of the specification can clarify, but
I would expect that the driver for the NPEM for a given storage device
would check it's local NPEM first and then walk up the hierarchy to
find the first NPEM instance enabled by platform firmware, then before
using that instance check if the ACPI _DSM is available for that PCI
device and use that instead. Otherwise, it's not clear to me how
software definitely associates a given control interface to a specific
storage, or memory in the case of CXL, device.

>
> >> +       help
> >> +         Driver for PCIe SSD status LED management as described in a PCI
> >> +         Firmware Specification, Revision 3.2 ECN.
> >
> > The auxiliary bus [1] was recently added as a way for drivers to carve
> > off functionality into sub-device / sub-driver pairs. One benefit from
> > the auxiliary bus organization is that the NPEM driver gets a propoer
> > alias and auto-loading support. As is this driver appears to need to be
> > manually loaded.
> >
> > [1]: Documentation/driver-api/auxiliary_bus.rst
> >
>
> Yes, unfortunately, it would need to be manually loaded.  I flip-flopped
> multiple times on making this just a library that the nvme driver could
> call to register the LEDs (if the _DSM was present).  It would be
> simpler, it wouldn't need a module to be manulaly loaded, it would
> resulted in better LED names (if the _DSM was on the NVMe PCI device),
> and it would mean that the driver that owns the PCI device would also
> own the LEDs.
>
> The only reason I didn't do that, is that the _DSM can be on the PCIe
> port that the NVMe device is connected to, rather than on the NVMe drive
> PCI device.  And I'm not sure how to deal with the latter situation,
> except maybe change the pcie port bus driver to also call in to the
> library to register LEDs somehow.  But maybe it would be worth trying to
> figure that out.

Like I mentioned above, I think it's ok / expected that the client of
this API will kick off a search that starts with the local PCI device
and walks the hierarchy if necessary to the first enabled NPEM
instance.

> I was not aware of the auxiliary bus, thanks for pointing it out.  I
> glanced over it, and I'm not sure how it would help over just having a
> library that nvme, pcie port bus driver, CXL, or whatever could call into.

It helps because it registers a typical device that can autoload the
NPEM driver. So, instead of force loading this module, or adding all
the logic to be called by NVME directly, the NVME driver can simply
register an auxiliary device. The CXL driver can load a similar
auxiliary device. Then the common NPEM driver would be autoloaded in
response to those events, and typical modprobe policy can be used to
filter the driver or apply any other policy upon the arrival of
NPEM-like devices.

> >> +
> >> +         When enabled, LED interfaces will be created for supported drive
> >> +         states for each PCIe device that has the ACPI _DSM method described
> >> +         in the referenced specification.
> >> diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
> >> index b2980db88cc0..fbcb8c2d1dc1 100644
> >> --- a/drivers/pci/pcie/Makefile
> >> +++ b/drivers/pci/pcie/Makefile
> >> @@ -13,3 +13,4 @@ obj-$(CONFIG_PCIE_PME)                += pme.o
> >>   obj-$(CONFIG_PCIE_DPC)         += dpc.o
> >>   obj-$(CONFIG_PCIE_PTM)         += ptm.o
> >>   obj-$(CONFIG_PCIE_EDR)         += edr.o
> >> +obj-$(CONFIG_PCIE_SSD_LEDS)    += ssd-leds.o
> >> diff --git a/drivers/pci/pcie/ssd-leds.c b/drivers/pci/pcie/ssd-leds.c
> >> new file mode 100644
> >> index 000000000000..cacb77e5da2d
> >> --- /dev/null
> >> +++ b/drivers/pci/pcie/ssd-leds.c
> >> @@ -0,0 +1,419 @@
> >> +// SPDX-License-Identifier: GPL-2.0-only
> >> +/*
> >> + * Module to provide LED interfaces for PCIe SSD status LED states, as
> >> + * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
> >> + * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
> >> + *
> >> + * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
> >> + * Management, but uses a _DSM ACPI method rather than a PCIe extended
> >> + * capability.
> >> + *
> >> + * Copyright (c) 2021 Dell Inc.
> >> + *
> >> + * TODO: Add NPEM support
> >> + */
> >> +
> >> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> >> +
> >> +#include <linux/acpi.h>
> >> +#include <linux/leds.h>
> >> +#include <linux/module.h>
> >> +#include <linux/mutex.h>
> >> +#include <linux/pci.h>
> >> +#include <uapi/linux/uleds.h>
> >> +
> >> +#define DRIVER_NAME    "pcie-ssd-leds"
> >> +#define DRIVER_VERSION "v1.0"
> >> +
> >> +struct led_state {
> >> +       char *name;
> >> +       int bit;
> >> +};
> >> +
> >> +static struct led_state led_states[] = {
> >> +       { .name = "ok",         .bit = 2 },
> >> +       { .name = "locate",     .bit = 3 },
> >> +       { .name = "failed",     .bit = 4 },
> >> +       { .name = "rebuild",    .bit = 5 },
> >> +       { .name = "pfa",        .bit = 6 },
> >> +       { .name = "hotspare",   .bit = 7 },
> >> +       { .name = "ica",        .bit = 8 },
> >> +       { .name = "ifa",        .bit = 9 },
> >> +       { .name = "invalid",    .bit = 10 },
> >> +       { .name = "disabled",   .bit = 11 },
> >> +};
> >
> > include/linux/enclosure.h has common ABI definitions of industry
> > standard enclosure LED settings. The above looks to be open coding the
> > same?
> >
>
> The LED states in inluce/linux/enclosure.h aren't exactly the same...
> there are states defined in NPEM/_DSM that aren't defined in
> enclosure.h.  In addition, while the enclosure driver allows "locate" to
> be controlled independently, it looks like it will only allow a single
> state (unsupported/ok/critical/etc) to be active at a time, while the
> NPEM/_DSM allow all of the state bits to be independently set or
> cleared.  Maybe only one of those states would need to be set at a time,
> I don't know, but that would impose a limitation on what NPEM/_DSM can
> do.  I'll take a closer look at this as an alternative to using
> drivers/leds/led-class.c.

Have a look. Maybe Mariusz can weigh in here with his experience with
ledmon with what capabilities ledmon would need from this driver so we
can decide what if any extensions need to be made to the enclosure
infrastructure?

It's been a while for me [2], but I seem to recall that some of the
enclosure specific functionality can be wrapped by the upper layer
driver like SES.

[2]: https://lore.kernel.org/all/CAPcyv4iaXfgB1PBv5v+dyRrUvQbzAyyFf1Ozsaao0t8K0w9zwg@mail.gmail.com/

[..]
> >> +static void initial_scan_for_leds(void)
> >> +{
> >> +       struct pci_dev *pdev = NULL;
> >> +
> >> +       for_each_pci_dev(pdev)
> >> +               probe_pdev(pdev);
> >
> >
> > This looks odd to me. Why force enable every occurrence these leds, and
> > do so indepdendently of the bind state of the driver for the associated
> > PCI device? I would expect that this support would be a library called
> > by the NVME driver, or the CXL driver. A library just like the
> > led_classdev infrastructure.
> >
>
> I guess I didn't know of any reason why they shouldn't be enabled even
> if the nvme driver wasn't bound, if this driver was stand-alone, since
> the LED function doesn't depend on the nvme driver.  But yes, maybe it
> would be better to make this a library that the nvme driver (or pcie
> port bus driver, or CXL driver) calls to register LEDs.

It's just not clear to me what userspace would do with this randomly
popping up without any clear association to a storage / memory device.
What reason would userpsace have to blink the lights in that
disassociated case?

> Thank you for taking a look at this... I'm very glad for any help to get
> this done.

No worries, thanks for taking this on, happy to help.
Pavel Machek Oct. 7, 2021, 8:24 a.m. UTC | #9
Hi!

> > Thank you for the comments, Dan.
> >
> > I originally did submit something simple that just added a couple of
> > sysfs attributes to allow userspace access to the _DSM, but Greg K-H
> > said (1) that I shouldn't create new driver-specific sysfs files that do
> > things that existing class drivers do, and that if I'm allowing LEDs to
> > be controlled by the user, I have to use the LED subsystem, so I went
> > with that. (See the end of
> > https://patchwork.ozlabs.org/project/linux-pci/patch/20201110153735.58587-1-stuart.w.hayes@gmail.com/)
> 
> I agree with the general sentiment to adopt and extend existing ABIs
> wherever possible, it's just not clear to me that the LED class driver
> is the best fit. The Enclosure class infrastructure, in addition to

LED drivers are not good fit for this. It is unlikely to be accepted.

Best regards,
								Pavel
Mariusz Tkaczyk Oct. 7, 2021, 11:32 a.m. UTC | #10
On 06.10.2021 22:15, Dan Williams wrote:
>>>> +static struct led_state led_states[] = {
>>>> +       { .name = "ok",         .bit = 2 },
>>>> +       { .name = "locate",     .bit = 3 },
>>>> +       { .name = "failed",     .bit = 4 },
>>>> +       { .name = "rebuild",    .bit = 5 },
>>>> +       { .name = "pfa",        .bit = 6 },
>>>> +       { .name = "hotspare",   .bit = 7 },
>>>> +       { .name = "ica",        .bit = 8 },
>>>> +       { .name = "ifa",        .bit = 9 },
>>>> +       { .name = "invalid",    .bit = 10 },
>>>> +       { .name = "disabled",   .bit = 11 },
>>>> +};
>>> include/linux/enclosure.h has common ABI definitions of industry
>>> standard enclosure LED settings. The above looks to be open coding the
>>> same?
>>>
>> The LED states in inluce/linux/enclosure.h aren't exactly the same...
>> there are states defined in NPEM/_DSM that aren't defined in
>> enclosure.h.  In addition, while the enclosure driver allows "locate" to
>> be controlled independently, it looks like it will only allow a single
>> state (unsupported/ok/critical/etc) to be active at a time, while the
>> NPEM/_DSM allow all of the state bits to be independently set or
>> cleared.  Maybe only one of those states would need to be set at a time,
>> I don't know, but that would impose a limitation on what NPEM/_DSM can
>> do.  I'll take a closer look at this as an alternative to using
>> drivers/leds/led-class.c.
> Have a look. Maybe Mariusz can weigh in here with his experience with
> ledmon with what capabilities ledmon would need from this driver so we
> can decide what if any extensions need to be made to the enclosure
> infrastructure?

Hmm... In ledmon we are expecting one state to be set at the time. So,
I would expected from kernel to work the same.

Looking into ledmon code, all capabilities from this list could be
used[1].

 >>>> +       { .name = "ok",         .bit = 2 },
 >>>> +       { .name = "locate",     .bit = 3 },
 >>>> +       { .name = "failed",     .bit = 4 },
 >>>> +       { .name = "rebuild",    .bit = 5 },
 >>>> +       { .name = "pfa",        .bit = 6 },
 >>>> +       { .name = "hotspare",   .bit = 7 },
 >>>> +       { .name = "ica",        .bit = 8 },
 >>>> +       { .name = "ifa",        .bit = 9 },

[1]https://github.com/intel/ledmon/blob/master/src/ibpi.h#L60

Thanks,
Mariusz
Stuart Hayes Nov. 2, 2021, 4:23 p.m. UTC | #11
On 10/7/2021 6:32 AM, Tkaczyk, Mariusz wrote:
> On 06.10.2021 22:15, Dan Williams wrote:
>>>>> +static struct led_state led_states[] = {
>>>>> +       { .name = "ok",         .bit = 2 },
>>>>> +       { .name = "locate",     .bit = 3 },
>>>>> +       { .name = "failed",     .bit = 4 },
>>>>> +       { .name = "rebuild",    .bit = 5 },
>>>>> +       { .name = "pfa",        .bit = 6 },
>>>>> +       { .name = "hotspare",   .bit = 7 },
>>>>> +       { .name = "ica",        .bit = 8 },
>>>>> +       { .name = "ifa",        .bit = 9 },
>>>>> +       { .name = "invalid",    .bit = 10 },
>>>>> +       { .name = "disabled",   .bit = 11 },
>>>>> +};
>>>> include/linux/enclosure.h has common ABI definitions of industry
>>>> standard enclosure LED settings. The above looks to be open coding the
>>>> same?
>>>>
>>> The LED states in inluce/linux/enclosure.h aren't exactly the same...
>>> there are states defined in NPEM/_DSM that aren't defined in
>>> enclosure.h.  In addition, while the enclosure driver allows "locate" to
>>> be controlled independently, it looks like it will only allow a single
>>> state (unsupported/ok/critical/etc) to be active at a time, while the
>>> NPEM/_DSM allow all of the state bits to be independently set or
>>> cleared.  Maybe only one of those states would need to be set at a time,
>>> I don't know, but that would impose a limitation on what NPEM/_DSM can
>>> do.  I'll take a closer look at this as an alternative to using
>>> drivers/leds/led-class.c.
>> Have a look. Maybe Mariusz can weigh in here with his experience with
>> ledmon with what capabilities ledmon would need from this driver so we
>> can decide what if any extensions need to be made to the enclosure
>> infrastructure?
> 
> Hmm... In ledmon we are expecting one state to be set at the time. So,
> I would expected from kernel to work the same.
> 
> Looking into ledmon code, all capabilities from this list could be
> used[1].
> 
>  >>>> +       { .name = "ok",         .bit = 2 },
>  >>>> +       { .name = "locate",     .bit = 3 },
>  >>>> +       { .name = "failed",     .bit = 4 },
>  >>>> +       { .name = "rebuild",    .bit = 5 },
>  >>>> +       { .name = "pfa",        .bit = 6 },
>  >>>> +       { .name = "hotspare",   .bit = 7 },
>  >>>> +       { .name = "ica",        .bit = 8 },
>  >>>> +       { .name = "ifa",        .bit = 9 },
> 
> [1]https://github.com/intel/ledmon/blob/master/src/ibpi.h#L60
> 
> Thanks,
> Mariusz

I've reworked the code so it is an auxiliary driver (to nvme, and
hopefully cxl and pcieport, but I haven't added that yet), and so it
registers as an enclosure (instead of using the LED subsystem).

I had no issues with making it an auxiliary driver... that seemed to
work well, and it looks like it should be easy to add cxl & pcieport
support.

Instead of making pcieport driver add an auxiliary device, I could make
this driver check the parent of the NVMe device if it doesn't find the
NPEM extended capability or _DSM on the NVMe PCIe device itself.  (I
didn't do that, because (a) it would be taking control of part of a PCIe
device to which a different driver was attached, and (b) the "invalid"
state isn't usable if the LED driver is only connected by the nvme driver.)

The enclosure driver isn't usable as is.  The "status" attribute for an
enclosure component corresponds to the "status code" in the SES spec (the
enclosure driver appears to be aligned to the SES spec)... the status code
is not what's used to to control the drive state indicators in SES--the
status code is read-only (in the SES spec) and reflects actual hardware
status, and there are other bits to control the indicators.

The enclosure driver currently only creates sysfs attributes for three
of the state indicators (active, locate, and fault).  So I added seven
more sysfs attributes for the other indicators (SES seems to support all
of the NPEM/_DSM indicators other than "invalid").

Below are the patches.  I wanted to post it here before I submit them,
in case this approach doesn't look good to anyone.  I also wonder if a
better name for this driver might be npem_dsm rather than ssd_leds...

Any feedback is appreciated, thank you!


---------------------------------------------------------
Patch to add auxiliary device to NVMe driver:
---------------------------------------------------------

diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c
index 456a0e8a5718..d9acb3132f43 100644
--- a/drivers/nvme/host/pci.c
+++ b/drivers/nvme/host/pci.c
@@ -26,6 +26,7 @@
  #include <linux/io-64-nonatomic-hi-lo.h>
  #include <linux/sed-opal.h>
  #include <linux/pci-p2pdma.h>
+#include <linux/auxiliary_bus.h>
  
  #include "trace.h"
  #include "nvme.h"
@@ -158,8 +159,38 @@ struct nvme_dev {
  	unsigned int nr_poll_queues;
  
  	bool attrs_added;
+
+	/* auxiliary_device for NPEM/_DSM driver for LEDs */
+	struct auxiliary_device ssd_leds_auxdev;
  };
  
+/*
+ * register auxiliary device for PCIe NPEM/_DSM status LEDs
+ */
+static void nvme_release_ssd_leds_aux_device(struct device *dev)
+{
+}
+
+static void nvme_register_ssd_leds_aux_device(struct nvme_dev *dev)
+{
+	struct auxiliary_device *adev;
+	int ret;
+
+	adev = &dev->ssd_leds_auxdev;
+	adev->name = "ssd_leds";
+	adev->dev.parent = dev->dev;
+	adev->dev.release = nvme_release_ssd_leds_aux_device;
+	adev->id = dev->ctrl.instance;
+	
+	ret = auxiliary_device_init(adev);
+	if (ret < 0)
+		return;
+
+	ret = auxiliary_device_add(adev);
+	if (ret)
+		auxiliary_device_uninit(adev);
+}
+
  static int io_queue_depth_set(const char *val, const struct kernel_param *kp)
  {
  	return param_set_uint_minmax(val, kp, NVME_PCI_MIN_QUEUE_SIZE,
@@ -2812,6 +2843,8 @@ static void nvme_reset_work(struct work_struct *work)
  		nvme_unfreeze(&dev->ctrl);
  	}
  
+	nvme_register_ssd_leds_aux_device(dev);
+
  	/*
  	 * If only admin queue live, keep it to do further investigation or
  	 * recovery.

---------------------------------------------------------
Patch to add more LED attributes to the enclosure driver:
---------------------------------------------------------

diff --git a/drivers/misc/enclosure.c b/drivers/misc/enclosure.c
index f950d0155876..95f4f500f4af 100644
--- a/drivers/misc/enclosure.c
+++ b/drivers/misc/enclosure.c
@@ -473,30 +473,6 @@ static const char *const enclosure_type[] = {
  	[ENCLOSURE_COMPONENT_ARRAY_DEVICE] = "array device",
  };
  
-static ssize_t get_component_fault(struct device *cdev,
-				   struct device_attribute *attr, char *buf)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-
-	if (edev->cb->get_fault)
-		edev->cb->get_fault(edev, ecomp);
-	return snprintf(buf, 40, "%d\n", ecomp->fault);
-}
-
-static ssize_t set_component_fault(struct device *cdev,
-				   struct device_attribute *attr,
-				   const char *buf, size_t count)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-	int val = simple_strtoul(buf, NULL, 0);
-
-	if (edev->cb->set_fault)
-		edev->cb->set_fault(edev, ecomp, val);
-	return count;
-}
-
  static ssize_t get_component_status(struct device *cdev,
  				    struct device_attribute *attr,char *buf)
  {
@@ -531,54 +507,6 @@ static ssize_t set_component_status(struct device *cdev,
  		return -EINVAL;
  }
  
-static ssize_t get_component_active(struct device *cdev,
-				    struct device_attribute *attr, char *buf)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-
-	if (edev->cb->get_active)
-		edev->cb->get_active(edev, ecomp);
-	return snprintf(buf, 40, "%d\n", ecomp->active);
-}
-
-static ssize_t set_component_active(struct device *cdev,
-				    struct device_attribute *attr,
-				    const char *buf, size_t count)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-	int val = simple_strtoul(buf, NULL, 0);
-
-	if (edev->cb->set_active)
-		edev->cb->set_active(edev, ecomp, val);
-	return count;
-}
-
-static ssize_t get_component_locate(struct device *cdev,
-				    struct device_attribute *attr, char *buf)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-
-	if (edev->cb->get_locate)
-		edev->cb->get_locate(edev, ecomp);
-	return snprintf(buf, 40, "%d\n", ecomp->locate);
-}
-
-static ssize_t set_component_locate(struct device *cdev,
-				    struct device_attribute *attr,
-				    const char *buf, size_t count)
-{
-	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
-	struct enclosure_component *ecomp = to_enclosure_component(cdev);
-	int val = simple_strtoul(buf, NULL, 0);
-
-	if (edev->cb->set_locate)
-		edev->cb->set_locate(edev, ecomp, val);
-	return count;
-}
-
  static ssize_t get_component_power_status(struct device *cdev,
  					  struct device_attribute *attr,
  					  char *buf)
@@ -641,30 +569,157 @@ static ssize_t get_component_slot(struct device *cdev,
  	return snprintf(buf, 40, "%d\n", slot);
  }
  
-static DEVICE_ATTR(fault, S_IRUGO | S_IWUSR, get_component_fault,
-		    set_component_fault);
+/*
+ * callbacks for attrs using enum enclosure_component_setting (LEDs)
+ */
+static ssize_t led_show(struct device *cdev,
+			enum enclosure_component_led led,
+			char *buf)
+{
+	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
+	struct enclosure_component *ecomp = to_enclosure_component(cdev);
+
+	if (edev->cb->get_led)
+		edev->cb->get_led(edev, ecomp, led);
+	else
+		/*
+		 * support old callbacks for fault/active/locate
+		 */
+		switch (led) {
+			case ENCLOSURE_LED_FAULT:
+				if (edev->cb->get_fault) {
+					edev->cb->get_fault(edev, ecomp);
+					ecomp->led[led] = ecomp->fault;
+				}
+				break;
+			case ENCLOSURE_LED_ACTIVE:
+				if (edev->cb->get_active) {
+					edev->cb->get_active(edev, ecomp);
+					ecomp->led[led] = ecomp->active;
+				}
+				break;
+			case ENCLOSURE_LED_LOCATE:
+				if (edev->cb->get_locate) {
+					edev->cb->get_locate(edev, ecomp);
+					ecomp->led[led] = ecomp->locate;
+				}
+				break;
+			default:
+		}
+
+	return snprintf(buf, 40, "%d\n", ecomp->led[led]);
+}
+
+static ssize_t led_set(struct device *cdev,
+		       enum enclosure_component_led led,
+		       const char *buf, size_t count)
+{
+	struct enclosure_device *edev = to_enclosure_device(cdev->parent);
+	struct enclosure_component *ecomp = to_enclosure_component(cdev);
+	int val = simple_strtoul(buf, NULL, 0);
+
+	if (edev->cb->set_led)
+		edev->cb->set_led(edev, ecomp, led, val);
+	else
+		/*
+		 * support old callbacks for fault/active/locate
+		 */
+		switch (led) {
+			case ENCLOSURE_LED_FAULT:
+				if (edev->cb->set_fault)
+					edev->cb->set_fault(edev, ecomp, val);
+				break;
+			case ENCLOSURE_LED_ACTIVE:
+				if (edev->cb->set_active)
+					edev->cb->set_active(edev, ecomp, val);
+				break;
+			case ENCLOSURE_LED_LOCATE:
+				if (edev->cb->set_locate)
+					edev->cb->set_locate(edev, ecomp, val);
+				break;
+			default:
+		}
+
+	return count;
+}
+
+#define define_led_rw_attr(name)	\
+static DEVICE_ATTR(name, 0644, show_##name, set_##name)
+
+#define LED_ATTR_RW(led_attr, led)		\
+static ssize_t show_##led_attr(struct device *cdev,			\
+			       struct device_attribute *attr,		\
+	       		       char *buf)				\
+{									\
+	return led_show(cdev, led, buf);				\
+}									\
+static ssize_t set_##led_attr(struct device *cdev,			\
+			      struct device_attribute *attr,		\
+			      const char *buf, size_t count)		\
+{									\
+	return led_set(cdev, led, buf, count);				\
+}									\
+define_led_rw_attr(led_attr)
+
  static DEVICE_ATTR(status, S_IRUGO | S_IWUSR, get_component_status,
  		   set_component_status);
-static DEVICE_ATTR(active, S_IRUGO | S_IWUSR, get_component_active,
-		   set_component_active);
-static DEVICE_ATTR(locate, S_IRUGO | S_IWUSR, get_component_locate,
-		   set_component_locate);
  static DEVICE_ATTR(power_status, S_IRUGO | S_IWUSR, get_component_power_status,
  		   set_component_power_status);
  static DEVICE_ATTR(type, S_IRUGO, get_component_type, NULL);
  static DEVICE_ATTR(slot, S_IRUGO, get_component_slot, NULL);
+LED_ATTR_RW(fault, ENCLOSURE_LED_FAULT);
+LED_ATTR_RW(active, ENCLOSURE_LED_ACTIVE);
+LED_ATTR_RW(locate, ENCLOSURE_LED_LOCATE);
+LED_ATTR_RW(ok, ENCLOSURE_LED_OK);
+LED_ATTR_RW(rebuild, ENCLOSURE_LED_REBUILD);
+LED_ATTR_RW(prdfail, ENCLOSURE_LED_PRDFAIL);
+LED_ATTR_RW(hotspare, ENCLOSURE_LED_HOTSPARE);
+LED_ATTR_RW(ica, ENCLOSURE_LED_ICA);
+LED_ATTR_RW(ifa, ENCLOSURE_LED_IFA);
+LED_ATTR_RW(disabled, ENCLOSURE_LED_DISABLED);
  
  static struct attribute *enclosure_component_attrs[] = {
  	&dev_attr_fault.attr,
  	&dev_attr_status.attr,
  	&dev_attr_active.attr,
  	&dev_attr_locate.attr,
+	&dev_attr_ok.attr,
+	&dev_attr_rebuild.attr,
+	&dev_attr_prdfail.attr,
+	&dev_attr_hotspare.attr,
+	&dev_attr_ica.attr,
+	&dev_attr_ifa.attr,
+	&dev_attr_disabled.attr,
  	&dev_attr_power_status.attr,
  	&dev_attr_type.attr,
  	&dev_attr_slot.attr,
  	NULL
  };
-ATTRIBUTE_GROUPS(enclosure_component);
+
+static umode_t enclosure_component_visible(struct kobject *kobj,
+					struct attribute *a, int n)
+{
+	struct device *dev = container_of(kobj, struct device, kobj);
+	struct enclosure_component *ecomp = to_enclosure_component(dev);
+
+	/*
+	 * hide attrs that are only available on array devices
+	 */
+	if (ecomp->type != ENCLOSURE_COMPONENT_ARRAY_DEVICE
+	    && (a == &dev_attr_rebuild.attr
+	        || a == &dev_attr_ok.attr
+	        || a == &dev_attr_hotspare.attr
+	        || a == &dev_attr_ifa.attr
+	        || a == &dev_attr_ica.attr))
+			return 0;
+	return a->mode;
+}
+
+static struct attribute_group enclosure_component_group = {
+	.attrs = enclosure_component_attrs,
+	.is_visible = enclosure_component_visible,
+};
+__ATTRIBUTE_GROUPS(enclosure_component);
  
  static int __init enclosure_init(void)
  {
diff --git a/include/linux/enclosure.h b/include/linux/enclosure.h
index 1c630e2c2756..db9dff77e595 100644
--- a/include/linux/enclosure.h
+++ b/include/linux/enclosure.h
@@ -49,6 +49,20 @@ enum enclosure_component_setting {
  	ENCLOSURE_SETTING_BLINK_B_OFF_ON = 7,
  };
  
+enum enclosure_component_led {
+	ENCLOSURE_LED_FAULT,
+	ENCLOSURE_LED_ACTIVE,
+	ENCLOSURE_LED_LOCATE,
+	ENCLOSURE_LED_OK,
+	ENCLOSURE_LED_REBUILD,
+	ENCLOSURE_LED_PRDFAIL,
+	ENCLOSURE_LED_HOTSPARE,
+	ENCLOSURE_LED_ICA,
+	ENCLOSURE_LED_IFA,
+	ENCLOSURE_LED_DISABLED,
+	ENCLOSURE_LED_MAX,
+};
+
  struct enclosure_device;
  struct enclosure_component;
  struct enclosure_component_callbacks {
@@ -72,6 +86,13 @@ struct enclosure_component_callbacks {
  	int (*set_locate)(struct enclosure_device *,
  			  struct enclosure_component *,
  			  enum enclosure_component_setting);
+	void (*get_led)(struct enclosure_device *,
+			struct enclosure_component *,
+			enum enclosure_component_led);
+	int (*set_led)(struct enclosure_device *,
+		       struct enclosure_component *,
+		       enum enclosure_component_led,
+		       enum enclosure_component_setting);
  	void (*get_power_status)(struct enclosure_device *,
  				 struct enclosure_component *);
  	int (*set_power_status)(struct enclosure_device *,
@@ -80,7 +101,6 @@ struct enclosure_component_callbacks {
  	int (*show_id)(struct enclosure_device *, char *buf);
  };
  
-
  struct enclosure_component {
  	void *scratch;
  	struct device cdev;
@@ -90,6 +110,7 @@ struct enclosure_component {
  	int fault;
  	int active;
  	int locate;
+	int led[ENCLOSURE_LED_MAX];
  	int slot;
  	enum enclosure_status status;
  	int power_status;

---------------------------------------------------------
Patch to add LED driver:
---------------------------------------------------------

diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 85ba901bc11b..e9c3b7f5236b 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -469,6 +469,17 @@ config HISI_HIKEY_USB
  	  switching between the dual-role USB-C port and the USB-A host ports
  	  using only one USB controller.
  
+config PCIE_SSD_LEDS
+	tristate "PCIe SSD status LED support"
+	depends on ACPI && ENCLOSURE_SERVICES
+	help
+	  Auxiliary driver for PCIe SSD status LED management as described in
+	  a PCI Firmware Specification, Revision 3.2 ECN.
+
+	  When enabled, an enclosure device will be created for each device
+	  that hast has the ACPI _DSM method described in the referenced ECN,
+	  to allow control of LED states.
+
  source "drivers/misc/c2port/Kconfig"
  source "drivers/misc/eeprom/Kconfig"
  source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index a086197af544..8a8cb438a78c 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -59,3 +59,4 @@ obj-$(CONFIG_UACCE)		+= uacce/
  obj-$(CONFIG_XILINX_SDFEC)	+= xilinx_sdfec.o
  obj-$(CONFIG_HISI_HIKEY_USB)	+= hisi_hikey_usb.o
  obj-$(CONFIG_HI6421V600_IRQ)	+= hi6421v600-irq.o
+obj-$(CONFIG_PCIE_SSD_LEDS)	+= ssd-leds.o
diff --git a/drivers/misc/ssd-leds.c b/drivers/misc/ssd-leds.c
new file mode 100644
index 000000000000..59927ebc8aa4
--- /dev/null
+++ b/drivers/misc/ssd-leds.c
@@ -0,0 +1,426 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Module to provide LED interfaces for PCIe SSD status LED states, as
+ * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
+ * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
+ *
+ * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
+ * Management, but uses a _DSM ACPI method rather than a PCIe extended
+ * capability.
+ *
+ * Copyright (c) 2021 Dell Inc.
+ *
+ * TODO: Add NPEM support
+ *       Add pcieport & cxl support
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/enclosure.h>
+#include <linux/auxiliary_bus.h>
+
+#define DRIVER_NAME	"ssd-leds"
+#define DRIVER_VERSION	"v1.0"
+
+/*
+ * NPEM & _DSM use the same state bits
+ */
+#define	NPEM_STATE_OK		BIT(2)
+#define	NPEM_STATE_LOCATE	BIT(3)
+#define	NPEM_STATE_FAILED	BIT(4)
+#define	NPEM_STATE_REBUILD	BIT(5)
+#define	NPEM_STATE_PFA		BIT(6)  /* predicted failure analysis */
+#define	NPEM_STATE_HOTSPARE	BIT(7)
+#define	NPEM_STATE_ICA		BIT(8)  /* in a critical array */
+#define	NPEM_STATE_IFA		BIT(9)  /* in a failed array */
+#define	NPEM_STATE_INVALID	BIT(10)
+#define	NPEM_STATE_DISABLED	BIT(11)
+
+static u32 to_npem_state[ENCLOSURE_LED_MAX];
+
+/*
+ * ssd_led_dev->dev could be the drive itself or its PCIe port
+ */
+struct ssd_led_dev {
+	struct list_head list;
+	/* PCI device that has the LED controls */
+	struct pci_dev *pdev;
+	/* ops to read/set state (NPEM or _DSM) */
+	struct drive_status_ops *ops;
+	/* enclosure device used as sysfs interface for states */
+	struct enclosure_device *edev;
+	/* latest written states */
+	u32 states;
+	u32 supported_states;
+};
+
+struct drive_status_ops {
+	int (*get_supported_states)(struct ssd_led_dev *sldev);
+	int (*get_current_states)(struct ssd_led_dev *sldev, u32 *states);
+	int (*set_current_states)(struct ssd_led_dev *sldev, u32 states);
+};
+
+static struct mutex drive_status_lock;
+static struct list_head ssd_led_dev_list;
+static int ssd_leds_exiting = 0;
+
+/*
+ * _DSM LED control
+ */
+static const guid_t pcie_ssd_leds_dsm_guid =
+	GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
+		  0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
+
+#define GET_SUPPORTED_STATES_DSM	0x01
+#define GET_STATE_DSM			0x02
+#define SET_STATE_DSM			0x03
+
+struct ssdleds_dsm_output {
+	u16 status;
+	u8 function_specific_err;
+	u8 vendor_specific_err;
+	u32 state;
+};
+
+static void dsm_status_err_print(struct pci_dev *pdev,
+				 struct ssdleds_dsm_output *output)
+{
+	switch (output->status) {
+	case 0:
+		break;
+	case 1:
+		pci_dbg(pdev, "_DSM not supported\n");
+		break;
+	case 2:
+		pci_dbg(pdev, "_DSM invalid input parameters\n");
+		break;
+	case 3:
+		pci_dbg(pdev, "_DSM communication error\n");
+		break;
+	case 4:
+		pci_dbg(pdev, "_DSM function-specific error 0x%x\n",
+			output->function_specific_err);
+		break;
+	case 5:
+		pci_dbg(pdev, "_DSM vendor-specific error 0x%x\n",
+			output->vendor_specific_err);
+		break;
+	default:
+		pci_dbg(pdev, "_DSM returned unknown status 0x%x\n",
+			output->status);
+	}
+}
+
+static int dsm_set(struct pci_dev *pdev, u32 value)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj, arg3[2];
+	struct ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return -ENODEV;
+
+	printk("dsm_set pdev %x:%x.%d value %x\n", pdev->bus->number, PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn), value);
+
+	arg3[0].type = ACPI_TYPE_PACKAGE;
+	arg3[0].package.count = 1;
+	arg3[0].package.elements = &arg3[1];
+
+	arg3[1].type = ACPI_TYPE_BUFFER;
+	arg3[1].buffer.length = 4;
+	arg3[1].buffer.pointer = (u8 *)&value;
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
+				1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
+
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(pdev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+	ACPI_FREE(out_obj);
+	return 0;
+}
+
+static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj;
+	struct ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return -ENODEV;
+
+	printk("dsm_get pdev %x:%x.%d\n", pdev->bus->number, PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn));
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
+					  dsm_func, NULL, ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(pdev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	*output = dsm_output->state;
+	ACPI_FREE(out_obj);
+	printk("...got %x\n", *output);
+	return 0;
+}
+
+static int get_supported_states_dsm(struct ssd_led_dev *sldev)
+{
+	return dsm_get(sldev->pdev, GET_SUPPORTED_STATES_DSM, &sldev->supported_states);
+}
+
+static int get_current_states_dsm(struct ssd_led_dev *sldev, u32 *states)
+{
+	return dsm_get(sldev->pdev, GET_STATE_DSM, states);
+}
+
+static int set_current_states_dsm(struct ssd_led_dev *sldev, u32 states)
+{
+	return dsm_set(sldev->pdev, states);
+}
+
+static bool pdev_has_dsm(struct pci_dev *pdev)
+{
+	acpi_handle handle = ACPI_HANDLE(&pdev->dev);
+
+	if (!handle)
+		return false;
+
+	return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
+			      1 << GET_SUPPORTED_STATES_DSM ||
+			      1 << GET_STATE_DSM ||
+			      1 << SET_STATE_DSM);
+}
+
+static struct drive_status_ops dsm_drive_status_ops = {
+	.get_supported_states = get_supported_states_dsm,
+	.get_current_states = get_current_states_dsm,
+	.set_current_states = set_current_states_dsm,
+};
+
+/*
+ * end of _DSM code
+ */
+
+static void ssd_leds_get_led(struct enclosure_device *edev,
+			     struct enclosure_component *ecomp,
+			     enum enclosure_component_led led)
+{
+	struct ssd_led_dev *sldev = ecomp->scratch;
+	u32 states = 0;
+
+	sldev->ops->get_current_states(sldev, &states);
+	ecomp->led[led] = !!(states & to_npem_state[led]) ?
+		ENCLOSURE_SETTING_ENABLED : ENCLOSURE_SETTING_DISABLED;
+}
+
+static int ssd_leds_set_led(struct enclosure_device *edev,
+			 struct enclosure_component *ecomp,
+			 enum enclosure_component_led led,
+			 enum enclosure_component_setting val)
+{
+	struct ssd_led_dev *sldev = ecomp->scratch;
+	u32 npem_state = to_npem_state[led], states;
+	int rc;
+
+	if (val != ENCLOSURE_SETTING_ENABLED
+	    && val != ENCLOSURE_SETTING_DISABLED)
+		return -EINVAL;
+
+	states = sldev->states & ~npem_state;
+	states |= val == ENCLOSURE_SETTING_ENABLED ? npem_state : 0;
+
+	if ((states & sldev->supported_states) != states) {
+		printk("can't set state %x\n", states);
+		return -EINVAL;
+	}
+
+	rc = sldev->ops->set_current_states(sldev, states);
+	/*
+	 * save last written state so it doesn't have to be re-read via NPEM/
+	 * _DSM on the next write, not only because it is faster, but because
+	 * _DSM doesn't provide a way to know if the last write has completed
+	 */
+	if (rc == 0)
+		sldev->states = states;
+	return rc;
+}
+
+static struct enclosure_component_callbacks ssdleds_cb = {
+	.get_led	= ssd_leds_get_led,
+	.set_led	= ssd_leds_set_led,
+};
+
+static struct ssd_led_dev *to_ssd_led_dev(struct pci_dev *pdev)
+{
+	struct ssd_led_dev *sldev;
+
+	list_for_each_entry(sldev, &ssd_led_dev_list, list)
+		if (pdev == sldev->pdev)
+			return sldev;
+	return NULL;
+}
+
+static void remove_ssd_led_dev(struct ssd_led_dev *sldev)
+{
+	enclosure_unregister(sldev->edev);
+	list_del(&sldev->list);
+	kfree(sldev);
+}
+
+static int add_ssd_led_dev(struct pci_dev *pdev,
+				const char *name,
+				struct drive_status_ops *ops)
+{
+	struct ssd_led_dev *sldev;
+	struct enclosure_device *edev;
+	struct enclosure_component *ecomp;
+	int rc = 0;
+
+	mutex_lock(&drive_status_lock);
+	if (ssd_leds_exiting)
+		goto out_unlock;
+
+	sldev = kzalloc(sizeof(*sldev), GFP_KERNEL);
+	if (!sldev) {
+		rc = -ENOMEM;
+		goto out_unlock;
+	}
+	sldev->pdev = pdev;
+	sldev->ops = ops;
+	sldev->states = 0;
+	INIT_LIST_HEAD(&sldev->list);
+	if (sldev->ops->get_supported_states(sldev) != 0)
+		goto out_free;
+
+	edev = enclosure_register(&pdev->dev, name, 1, &ssdleds_cb);
+	if (!edev)
+		goto out_free;
+
+	ecomp = enclosure_component_alloc(edev, 0, ENCLOSURE_COMPONENT_DEVICE, "leds");
+	if (IS_ERR(ecomp))
+		goto out_unreg;
+
+	ecomp->type = ENCLOSURE_COMPONENT_ARRAY_DEVICE;
+	rc = enclosure_component_register(ecomp);
+	if (rc < 0)
+		goto out_unreg;
+
+	ecomp->scratch = sldev;
+	sldev->edev = edev;
+	list_add_tail(&sldev->list, &ssd_led_dev_list);
+	goto out_unlock;
+
+out_unreg:
+	enclosure_unregister(edev);
+out_free:
+	kfree(sldev);
+out_unlock:
+	mutex_unlock(&drive_status_lock);
+	return rc;
+}
+
+static int ssd_leds_probe(struct auxiliary_device *adev,
+			  const struct auxiliary_device_id *id)
+{
+	struct pci_dev *pdev = to_pci_dev(adev->dev.parent);
+
+	if (pdev_has_dsm(pdev))
+		return add_ssd_led_dev(pdev, dev_name(&adev->dev), &dsm_drive_status_ops);
+	return 0;
+}
+
+static void ssd_leds_remove(struct auxiliary_device *adev)
+{
+	struct pci_dev *pdev = to_pci_dev(adev->dev.parent);
+	struct ssd_led_dev *sldev;
+
+	mutex_lock(&drive_status_lock);
+	sldev = to_ssd_led_dev(pdev);
+	if (sldev)
+		remove_ssd_led_dev(sldev);
+out_unlock:
+	mutex_unlock(&drive_status_lock);
+}
+
+static const struct auxiliary_device_id ssd_leds_id_table[] = {
+	{ .name = "nvme.ssd_leds", },
+//	{ .name = "pcieport.ssd_leds", },
+//	{ .name = "cxl.ssd_leds", },	
+	{},
+};
+
+MODULE_DEVICE_TABLE(auxiliary, ssd_leds_id_table);
+
+static struct auxiliary_driver ssd_leds_driver = {
+	.name = "ssd_leds",
+	.probe = ssd_leds_probe,
+	.remove = ssd_leds_remove,
+	.id_table = ssd_leds_id_table,
+};
+
+static int __init ssd_leds_init(void)
+{
+	mutex_init(&drive_status_lock);
+	INIT_LIST_HEAD(&ssd_led_dev_list);
+
+	to_npem_state[ENCLOSURE_LED_FAULT] = NPEM_STATE_FAILED;
+	to_npem_state[ENCLOSURE_LED_ACTIVE] = 0;
+	to_npem_state[ENCLOSURE_LED_LOCATE] = NPEM_STATE_LOCATE;
+	to_npem_state[ENCLOSURE_LED_OK] = NPEM_STATE_OK;
+	to_npem_state[ENCLOSURE_LED_REBUILD] = NPEM_STATE_REBUILD;
+	to_npem_state[ENCLOSURE_LED_PRDFAIL] = NPEM_STATE_PFA;
+	to_npem_state[ENCLOSURE_LED_HOTSPARE] = NPEM_STATE_HOTSPARE;
+	to_npem_state[ENCLOSURE_LED_ICA] = NPEM_STATE_ICA;
+	to_npem_state[ENCLOSURE_LED_IFA] = NPEM_STATE_IFA;
+	to_npem_state[ENCLOSURE_LED_DISABLED] = NPEM_STATE_DISABLED;
+
+	auxiliary_driver_register(&ssd_leds_driver);
+	return 0;
+}
+
+static void __exit ssd_leds_exit(void)
+{
+	struct ssd_led_dev *sldev, *temp;
+
+	mutex_lock(&drive_status_lock);
+	ssd_leds_exiting = 1;
+	list_for_each_entry_safe(sldev, temp, &ssd_led_dev_list, list)
+		remove_ssd_led_dev(sldev);
+	mutex_unlock(&drive_status_lock);
+	auxiliary_driver_unregister(&ssd_leds_driver);
+}
+
+module_init(ssd_leds_init);
+module_exit(ssd_leds_exit);
+
+MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
+MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
+MODULE_LICENSE("GPL");
Dan Williams Nov. 6, 2021, 2:52 a.m. UTC | #12
[ add James and Martin in case they see anything here that collides
with the SES driver ]

On Tue, Nov 2, 2021 at 9:34 AM stuart hayes <stuart.w.hayes@gmail.com> wrote:
>
>
>
> On 10/7/2021 6:32 AM, Tkaczyk, Mariusz wrote:
> > On 06.10.2021 22:15, Dan Williams wrote:
> >>>>> +static struct led_state led_states[] = {
> >>>>> +       { .name = "ok",         .bit = 2 },
> >>>>> +       { .name = "locate",     .bit = 3 },
> >>>>> +       { .name = "failed",     .bit = 4 },
> >>>>> +       { .name = "rebuild",    .bit = 5 },
> >>>>> +       { .name = "pfa",        .bit = 6 },
> >>>>> +       { .name = "hotspare",   .bit = 7 },
> >>>>> +       { .name = "ica",        .bit = 8 },
> >>>>> +       { .name = "ifa",        .bit = 9 },
> >>>>> +       { .name = "invalid",    .bit = 10 },
> >>>>> +       { .name = "disabled",   .bit = 11 },
> >>>>> +};
> >>>> include/linux/enclosure.h has common ABI definitions of industry
> >>>> standard enclosure LED settings. The above looks to be open coding the
> >>>> same?
> >>>>
> >>> The LED states in inluce/linux/enclosure.h aren't exactly the same...
> >>> there are states defined in NPEM/_DSM that aren't defined in
> >>> enclosure.h.  In addition, while the enclosure driver allows "locate" to
> >>> be controlled independently, it looks like it will only allow a single
> >>> state (unsupported/ok/critical/etc) to be active at a time, while the
> >>> NPEM/_DSM allow all of the state bits to be independently set or
> >>> cleared.  Maybe only one of those states would need to be set at a time,
> >>> I don't know, but that would impose a limitation on what NPEM/_DSM can
> >>> do.  I'll take a closer look at this as an alternative to using
> >>> drivers/leds/led-class.c.
> >> Have a look. Maybe Mariusz can weigh in here with his experience with
> >> ledmon with what capabilities ledmon would need from this driver so we
> >> can decide what if any extensions need to be made to the enclosure
> >> infrastructure?
> >
> > Hmm... In ledmon we are expecting one state to be set at the time. So,
> > I would expected from kernel to work the same.
> >
> > Looking into ledmon code, all capabilities from this list could be
> > used[1].
> >
> >  >>>> +       { .name = "ok",         .bit = 2 },
> >  >>>> +       { .name = "locate",     .bit = 3 },
> >  >>>> +       { .name = "failed",     .bit = 4 },
> >  >>>> +       { .name = "rebuild",    .bit = 5 },
> >  >>>> +       { .name = "pfa",        .bit = 6 },
> >  >>>> +       { .name = "hotspare",   .bit = 7 },
> >  >>>> +       { .name = "ica",        .bit = 8 },
> >  >>>> +       { .name = "ifa",        .bit = 9 },
> >
> > [1]https://github.com/intel/ledmon/blob/master/src/ibpi.h#L60
> >
> > Thanks,
> > Mariusz
>
> I've reworked the code so it is an auxiliary driver (to nvme, and
> hopefully cxl and pcieport, but I haven't added that yet), and so it
> registers as an enclosure (instead of using the LED subsystem).
>
> I had no issues with making it an auxiliary driver... that seemed to
> work well, and it looks like it should be easy to add cxl & pcieport
> support.

Cool! Thanks for taking this on.

>
> Instead of making pcieport driver add an auxiliary device, I could make
> this driver check the parent of the NVMe device if it doesn't find the
> NPEM extended capability or _DSM on the NVMe PCIe device itself.  (I
> didn't do that, because (a) it would be taking control of part of a PCIe
> device to which a different driver was attached, and (b) the "invalid"
> state isn't usable if the LED driver is only connected by the nvme driver.)

Caveat a) does not sound too bad to me, but b) does sound unfortunate.

What about a mechanism for a downstream driver to register itself
after the fact with the active NPEM instant in its parent topology? At
least I seem to recall either enclosure or ses linking a block-device
to an enclosure slot.

> The enclosure driver isn't usable as is.  The "status" attribute for an
> enclosure component corresponds to the "status code" in the SES spec (the
> enclosure driver appears to be aligned to the SES spec)... the status code
> is not what's used to to control the drive state indicators in SES--the
> status code is read-only (in the SES spec) and reflects actual hardware
> status, and there are other bits to control the indicators.

What does this mean for the NPEM enabling?

> The enclosure driver currently only creates sysfs attributes for three
> of the state indicators (active, locate, and fault).  So I added seven
> more sysfs attributes for the other indicators (SES seems to support all
> of the NPEM/_DSM indicators other than "invalid").
>
> Below are the patches.  I wanted to post it here before I submit them,
> in case this approach doesn't look good to anyone.

If you want to get early opinions an "RFC" prefix on the submission
helps set the expectations of the implementation maturity. It's also
nicer to see patches with changelogs, but no worries, I'll take a look
at the below.

> I also wonder if a
> better name for this driver might be npem_dsm rather than ssd_leds...

I do think dropping the "SSD" connotation is useful. Either NPEM or
PCIE_EM sound ok to me. No need for the _DSM suffix I think, that's
just an implementation detail that platform firmware can get in the
way. I can also imagine a non-ACPI platform doing a similar override,
but it's all NPEM protocol in the end.

>
> Any feedback is appreciated, thank you!
>
>
> ---------------------------------------------------------
> Patch to add auxiliary device to NVMe driver:
> ---------------------------------------------------------
>
> diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c
> index 456a0e8a5718..d9acb3132f43 100644
> --- a/drivers/nvme/host/pci.c
> +++ b/drivers/nvme/host/pci.c
> @@ -26,6 +26,7 @@
>   #include <linux/io-64-nonatomic-hi-lo.h>
>   #include <linux/sed-opal.h>
>   #include <linux/pci-p2pdma.h>
> +#include <linux/auxiliary_bus.h>
>
>   #include "trace.h"
>   #include "nvme.h"
> @@ -158,8 +159,38 @@ struct nvme_dev {
>         unsigned int nr_poll_queues;
>
>         bool attrs_added;
> +
> +       /* auxiliary_device for NPEM/_DSM driver for LEDs */
> +       struct auxiliary_device ssd_leds_auxdev;

This auxiliary device needs to be allocated out of line from 'struct
nvme_dev'. The lifetime of 'struct nvme_dev' is shorter than a 'struct
device'. Recall that an auxiliary device embeds a 'struct device', and
a 'struct device' embeds a kobject. The kobject lifetime can be
arbitrarily extended by any random kernel agent taking a reference on
the object. You can likely trigger use after free warnings with this
code if you enable CONFIG_DEBUG_KOBJECT_RELEASE.


>   };
>
> +/*
> + * register auxiliary device for PCIe NPEM/_DSM status LEDs
> + */
> +static void nvme_release_ssd_leds_aux_device(struct device *dev)
> +{

Empty device release method is among the most common bugs that Greg
finds, be sure to fix this up before sending he sees it. The above
suggestion to move the allocation of the device out of line will fix
this up too.

> +}
> +
> +static void nvme_register_ssd_leds_aux_device(struct nvme_dev *dev)
> +{
> +       struct auxiliary_device *adev;
> +       int ret;
> +
> +       adev = &dev->ssd_leds_auxdev;
> +       adev->name = "ssd_leds";
> +       adev->dev.parent = dev->dev;
> +       adev->dev.release = nvme_release_ssd_leds_aux_device;
> +       adev->id = dev->ctrl.instance;
> +
> +       ret = auxiliary_device_init(adev);
> +       if (ret < 0)
> +               return;
> +
> +       ret = auxiliary_device_add(adev);
> +       if (ret)
> +               auxiliary_device_uninit(adev);
> +}
> +
>   static int io_queue_depth_set(const char *val, const struct kernel_param *kp)
>   {
>         return param_set_uint_minmax(val, kp, NVME_PCI_MIN_QUEUE_SIZE,
> @@ -2812,6 +2843,8 @@ static void nvme_reset_work(struct work_struct *work)
>                 nvme_unfreeze(&dev->ctrl);
>         }
>
> +       nvme_register_ssd_leds_aux_device(dev);
> +
>         /*
>          * If only admin queue live, keep it to do further investigation or
>          * recovery.
>
> ---------------------------------------------------------
> Patch to add more LED attributes to the enclosure driver:
> ---------------------------------------------------------
>
> diff --git a/drivers/misc/enclosure.c b/drivers/misc/enclosure.c
> index f950d0155876..95f4f500f4af 100644
> --- a/drivers/misc/enclosure.c
> +++ b/drivers/misc/enclosure.c
> @@ -473,30 +473,6 @@ static const char *const enclosure_type[] = {
>         [ENCLOSURE_COMPONENT_ARRAY_DEVICE] = "array device",
>   };
>
> -static ssize_t get_component_fault(struct device *cdev,
> -                                  struct device_attribute *attr, char *buf)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -
> -       if (edev->cb->get_fault)
> -               edev->cb->get_fault(edev, ecomp);
> -       return snprintf(buf, 40, "%d\n", ecomp->fault);
> -}

This conversion to the LED_ATTR_RW() scheme definitely wants to be its
own lead-in cleanup patch with a changelog before adding the new NPEM
attributes. Don't smoosh it all into one patch when you reformat this
into a patch series.

> -
> -static ssize_t set_component_fault(struct device *cdev,
> -                                  struct device_attribute *attr,
> -                                  const char *buf, size_t count)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -       int val = simple_strtoul(buf, NULL, 0);
> -
> -       if (edev->cb->set_fault)
> -               edev->cb->set_fault(edev, ecomp, val);
> -       return count;
> -}
> -
>   static ssize_t get_component_status(struct device *cdev,
>                                     struct device_attribute *attr,char *buf)
>   {
> @@ -531,54 +507,6 @@ static ssize_t set_component_status(struct device *cdev,
>                 return -EINVAL;
>   }
>
> -static ssize_t get_component_active(struct device *cdev,
> -                                   struct device_attribute *attr, char *buf)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -
> -       if (edev->cb->get_active)
> -               edev->cb->get_active(edev, ecomp);
> -       return snprintf(buf, 40, "%d\n", ecomp->active);
> -}
> -
> -static ssize_t set_component_active(struct device *cdev,
> -                                   struct device_attribute *attr,
> -                                   const char *buf, size_t count)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -       int val = simple_strtoul(buf, NULL, 0);
> -
> -       if (edev->cb->set_active)
> -               edev->cb->set_active(edev, ecomp, val);
> -       return count;
> -}
> -
> -static ssize_t get_component_locate(struct device *cdev,
> -                                   struct device_attribute *attr, char *buf)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -
> -       if (edev->cb->get_locate)
> -               edev->cb->get_locate(edev, ecomp);
> -       return snprintf(buf, 40, "%d\n", ecomp->locate);
> -}
> -
> -static ssize_t set_component_locate(struct device *cdev,
> -                                   struct device_attribute *attr,
> -                                   const char *buf, size_t count)
> -{
> -       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> -       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> -       int val = simple_strtoul(buf, NULL, 0);
> -
> -       if (edev->cb->set_locate)
> -               edev->cb->set_locate(edev, ecomp, val);
> -       return count;
> -}
> -
>   static ssize_t get_component_power_status(struct device *cdev,
>                                           struct device_attribute *attr,
>                                           char *buf)
> @@ -641,30 +569,157 @@ static ssize_t get_component_slot(struct device *cdev,
>         return snprintf(buf, 40, "%d\n", slot);
>   }
>
> -static DEVICE_ATTR(fault, S_IRUGO | S_IWUSR, get_component_fault,
> -                   set_component_fault);
> +/*
> + * callbacks for attrs using enum enclosure_component_setting (LEDs)
> + */
> +static ssize_t led_show(struct device *cdev,
> +                       enum enclosure_component_led led,
> +                       char *buf)
> +{
> +       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> +       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> +
> +       if (edev->cb->get_led)
> +               edev->cb->get_led(edev, ecomp, led);
> +       else
> +               /*
> +                * support old callbacks for fault/active/locate
> +                */
> +               switch (led) {
> +                       case ENCLOSURE_LED_FAULT:
> +                               if (edev->cb->get_fault) {
> +                                       edev->cb->get_fault(edev, ecomp);
> +                                       ecomp->led[led] = ecomp->fault;
> +                               }
> +                               break;
> +                       case ENCLOSURE_LED_ACTIVE:
> +                               if (edev->cb->get_active) {
> +                                       edev->cb->get_active(edev, ecomp);
> +                                       ecomp->led[led] = ecomp->active;
> +                               }
> +                               break;
> +                       case ENCLOSURE_LED_LOCATE:
> +                               if (edev->cb->get_locate) {
> +                                       edev->cb->get_locate(edev, ecomp);
> +                                       ecomp->led[led] = ecomp->locate;
> +                               }
> +                               break;
> +                       default:
> +               }
> +
> +       return snprintf(buf, 40, "%d\n", ecomp->led[led]);
> +}
> +
> +static ssize_t led_set(struct device *cdev,
> +                      enum enclosure_component_led led,
> +                      const char *buf, size_t count)
> +{
> +       struct enclosure_device *edev = to_enclosure_device(cdev->parent);
> +       struct enclosure_component *ecomp = to_enclosure_component(cdev);
> +       int val = simple_strtoul(buf, NULL, 0);
> +
> +       if (edev->cb->set_led)
> +               edev->cb->set_led(edev, ecomp, led, val);
> +       else
> +               /*
> +                * support old callbacks for fault/active/locate
> +                */
> +               switch (led) {
> +                       case ENCLOSURE_LED_FAULT:
> +                               if (edev->cb->set_fault)
> +                                       edev->cb->set_fault(edev, ecomp, val);
> +                               break;
> +                       case ENCLOSURE_LED_ACTIVE:
> +                               if (edev->cb->set_active)
> +                                       edev->cb->set_active(edev, ecomp, val);
> +                               break;
> +                       case ENCLOSURE_LED_LOCATE:
> +                               if (edev->cb->set_locate)
> +                                       edev->cb->set_locate(edev, ecomp, val);
> +                               break;
> +                       default:
> +               }
> +
> +       return count;
> +}
> +
> +#define define_led_rw_attr(name)       \
> +static DEVICE_ATTR(name, 0644, show_##name, set_##name)
> +
> +#define LED_ATTR_RW(led_attr, led)             \
> +static ssize_t show_##led_attr(struct device *cdev,                    \
> +                              struct device_attribute *attr,           \
> +                              char *buf)                               \
> +{                                                                      \
> +       return led_show(cdev, led, buf);                                \
> +}                                                                      \
> +static ssize_t set_##led_attr(struct device *cdev,                     \
> +                             struct device_attribute *attr,            \
> +                             const char *buf, size_t count)            \
> +{                                                                      \
> +       return led_set(cdev, led, buf, count);                          \
> +}                                                                      \
> +define_led_rw_attr(led_attr)
> +
>   static DEVICE_ATTR(status, S_IRUGO | S_IWUSR, get_component_status,
>                    set_component_status);
> -static DEVICE_ATTR(active, S_IRUGO | S_IWUSR, get_component_active,
> -                  set_component_active);
> -static DEVICE_ATTR(locate, S_IRUGO | S_IWUSR, get_component_locate,
> -                  set_component_locate);
>   static DEVICE_ATTR(power_status, S_IRUGO | S_IWUSR, get_component_power_status,
>                    set_component_power_status);
>   static DEVICE_ATTR(type, S_IRUGO, get_component_type, NULL);
>   static DEVICE_ATTR(slot, S_IRUGO, get_component_slot, NULL);
> +LED_ATTR_RW(fault, ENCLOSURE_LED_FAULT);
> +LED_ATTR_RW(active, ENCLOSURE_LED_ACTIVE);
> +LED_ATTR_RW(locate, ENCLOSURE_LED_LOCATE);
> +LED_ATTR_RW(ok, ENCLOSURE_LED_OK);
> +LED_ATTR_RW(rebuild, ENCLOSURE_LED_REBUILD);
> +LED_ATTR_RW(prdfail, ENCLOSURE_LED_PRDFAIL);
> +LED_ATTR_RW(hotspare, ENCLOSURE_LED_HOTSPARE);
> +LED_ATTR_RW(ica, ENCLOSURE_LED_ICA);
> +LED_ATTR_RW(ifa, ENCLOSURE_LED_IFA);
> +LED_ATTR_RW(disabled, ENCLOSURE_LED_DISABLED);
>
>   static struct attribute *enclosure_component_attrs[] = {
>         &dev_attr_fault.attr,
>         &dev_attr_status.attr,
>         &dev_attr_active.attr,
>         &dev_attr_locate.attr,
> +       &dev_attr_ok.attr,
> +       &dev_attr_rebuild.attr,
> +       &dev_attr_prdfail.attr,
> +       &dev_attr_hotspare.attr,
> +       &dev_attr_ica.attr,
> +       &dev_attr_ifa.attr,
> +       &dev_attr_disabled.attr,
>         &dev_attr_power_status.attr,
>         &dev_attr_type.attr,
>         &dev_attr_slot.attr,
>         NULL
>   };
> -ATTRIBUTE_GROUPS(enclosure_component);
> +
> +static umode_t enclosure_component_visible(struct kobject *kobj,
> +                                       struct attribute *a, int n)
> +{
> +       struct device *dev = container_of(kobj, struct device, kobj);
> +       struct enclosure_component *ecomp = to_enclosure_component(dev);
> +
> +       /*
> +        * hide attrs that are only available on array devices
> +        */

Isn't this expanding the ABI for enclosures outside of NPEM? Should
there be a new ENCLOSURE_COMPONENT_NPEM or somesuch?

> +       if (ecomp->type != ENCLOSURE_COMPONENT_ARRAY_DEVICE
> +           && (a == &dev_attr_rebuild.attr
> +               || a == &dev_attr_ok.attr
> +               || a == &dev_attr_hotspare.attr
> +               || a == &dev_attr_ifa.attr
> +               || a == &dev_attr_ica.attr))
> +                       return 0;
> +       return a->mode;
> +}
> +
> +static struct attribute_group enclosure_component_group = {

const?

> +       .attrs = enclosure_component_attrs,
> +       .is_visible = enclosure_component_visible,
> +};
> +__ATTRIBUTE_GROUPS(enclosure_component);
>
>   static int __init enclosure_init(void)
>   {
> diff --git a/include/linux/enclosure.h b/include/linux/enclosure.h
> index 1c630e2c2756..db9dff77e595 100644
> --- a/include/linux/enclosure.h
> +++ b/include/linux/enclosure.h
> @@ -49,6 +49,20 @@ enum enclosure_component_setting {
>         ENCLOSURE_SETTING_BLINK_B_OFF_ON = 7,
>   };
>
> +enum enclosure_component_led {
> +       ENCLOSURE_LED_FAULT,
> +       ENCLOSURE_LED_ACTIVE,
> +       ENCLOSURE_LED_LOCATE,
> +       ENCLOSURE_LED_OK,
> +       ENCLOSURE_LED_REBUILD,
> +       ENCLOSURE_LED_PRDFAIL,
> +       ENCLOSURE_LED_HOTSPARE,
> +       ENCLOSURE_LED_ICA,
> +       ENCLOSURE_LED_IFA,
> +       ENCLOSURE_LED_DISABLED,
> +       ENCLOSURE_LED_MAX,

I didn't quite understand why this needs to be distinct from 'enum
enclosure_status'?

> +};
> +
>   struct enclosure_device;
>   struct enclosure_component;
>   struct enclosure_component_callbacks {
> @@ -72,6 +86,13 @@ struct enclosure_component_callbacks {
>         int (*set_locate)(struct enclosure_device *,
>                           struct enclosure_component *,
>                           enum enclosure_component_setting);
> +       void (*get_led)(struct enclosure_device *,
> +                       struct enclosure_component *,
> +                       enum enclosure_component_led);
> +       int (*set_led)(struct enclosure_device *,
> +                      struct enclosure_component *,
> +                      enum enclosure_component_led,
> +                      enum enclosure_component_setting);
>         void (*get_power_status)(struct enclosure_device *,
>                                  struct enclosure_component *);
>         int (*set_power_status)(struct enclosure_device *,
> @@ -80,7 +101,6 @@ struct enclosure_component_callbacks {
>         int (*show_id)(struct enclosure_device *, char *buf);
>   };
>
> -
>   struct enclosure_component {
>         void *scratch;
>         struct device cdev;
> @@ -90,6 +110,7 @@ struct enclosure_component {
>         int fault;
>         int active;
>         int locate;
> +       int led[ENCLOSURE_LED_MAX];
>         int slot;
>         enum enclosure_status status;
>         int power_status;
>
> ---------------------------------------------------------
> Patch to add LED driver:
> ---------------------------------------------------------
>
> diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
> index 85ba901bc11b..e9c3b7f5236b 100644
> --- a/drivers/misc/Kconfig
> +++ b/drivers/misc/Kconfig
> @@ -469,6 +469,17 @@ config HISI_HIKEY_USB
>           switching between the dual-role USB-C port and the USB-A host ports
>           using only one USB controller.
>
> +config PCIE_SSD_LEDS
> +       tristate "PCIe SSD status LED support"
> +       depends on ACPI && ENCLOSURE_SERVICES

Let's find a way to gracefully fall back to NPEM-only in the case of
CONFIG_ACPI=n builds. I.e. POWERPC platforms have NPEM but not ACPI.

> +       help
> +         Auxiliary driver for PCIe SSD status LED management as described in
> +         a PCI Firmware Specification, Revision 3.2 ECN.
> +
> +         When enabled, an enclosure device will be created for each device
> +         that hast has the ACPI _DSM method described in the referenced ECN,
> +         to allow control of LED states.
> +
>   source "drivers/misc/c2port/Kconfig"
>   source "drivers/misc/eeprom/Kconfig"
>   source "drivers/misc/cb710/Kconfig"
> diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
> index a086197af544..8a8cb438a78c 100644
> --- a/drivers/misc/Makefile
> +++ b/drivers/misc/Makefile
> @@ -59,3 +59,4 @@ obj-$(CONFIG_UACCE)           += uacce/
>   obj-$(CONFIG_XILINX_SDFEC)    += xilinx_sdfec.o
>   obj-$(CONFIG_HISI_HIKEY_USB)  += hisi_hikey_usb.o
>   obj-$(CONFIG_HI6421V600_IRQ)  += hi6421v600-irq.o
> +obj-$(CONFIG_PCIE_SSD_LEDS)    += ssd-leds.o
> diff --git a/drivers/misc/ssd-leds.c b/drivers/misc/ssd-leds.c
> new file mode 100644
> index 000000000000..59927ebc8aa4
> --- /dev/null
> +++ b/drivers/misc/ssd-leds.c

I think this belongs in drivers/pci/ and / or drivers/acpi/. I.e the
enumeration code could enumerate either an ACPI auxdev or a PCIE
auxdev and register a separate driver accordingly with a shared NPEM
core for the common bits.

> @@ -0,0 +1,426 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Module to provide LED interfaces for PCIe SSD status LED states, as
> + * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
> + * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
> + *
> + * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
> + * Management, but uses a _DSM ACPI method rather than a PCIe extended
> + * capability.
> + *
> + * Copyright (c) 2021 Dell Inc.
> + *
> + * TODO: Add NPEM support
> + *       Add pcieport & cxl support
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/pci.h>
> +#include <linux/enclosure.h>
> +#include <linux/auxiliary_bus.h>
> +
> +#define DRIVER_NAME    "ssd-leds"
> +#define DRIVER_VERSION "v1.0"
> +
> +/*
> + * NPEM & _DSM use the same state bits
> + */
> +#define        NPEM_STATE_OK           BIT(2)
> +#define        NPEM_STATE_LOCATE       BIT(3)
> +#define        NPEM_STATE_FAILED       BIT(4)
> +#define        NPEM_STATE_REBUILD      BIT(5)
> +#define        NPEM_STATE_PFA          BIT(6)  /* predicted failure analysis */
> +#define        NPEM_STATE_HOTSPARE     BIT(7)
> +#define        NPEM_STATE_ICA          BIT(8)  /* in a critical array */
> +#define        NPEM_STATE_IFA          BIT(9)  /* in a failed array */
> +#define        NPEM_STATE_INVALID      BIT(10)
> +#define        NPEM_STATE_DISABLED     BIT(11)
> +
> +static u32 to_npem_state[ENCLOSURE_LED_MAX];
> +
> +/*
> + * ssd_led_dev->dev could be the drive itself or its PCIe port
> + */
> +struct ssd_led_dev {
> +       struct list_head list;
> +       /* PCI device that has the LED controls */
> +       struct pci_dev *pdev;
> +       /* ops to read/set state (NPEM or _DSM) */
> +       struct drive_status_ops *ops;
> +       /* enclosure device used as sysfs interface for states */
> +       struct enclosure_device *edev;
> +       /* latest written states */
> +       u32 states;
> +       u32 supported_states;
> +};
> +
> +struct drive_status_ops {
> +       int (*get_supported_states)(struct ssd_led_dev *sldev);
> +       int (*get_current_states)(struct ssd_led_dev *sldev, u32 *states);
> +       int (*set_current_states)(struct ssd_led_dev *sldev, u32 states);
> +};
> +
> +static struct mutex drive_status_lock;
> +static struct list_head ssd_led_dev_list;
> +static int ssd_leds_exiting = 0;
> +
> +/*
> + * _DSM LED control
> + */
> +static const guid_t pcie_ssd_leds_dsm_guid =
> +       GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
> +                 0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
> +
> +#define GET_SUPPORTED_STATES_DSM       0x01
> +#define GET_STATE_DSM                  0x02
> +#define SET_STATE_DSM                  0x03
> +
> +struct ssdleds_dsm_output {
> +       u16 status;
> +       u8 function_specific_err;
> +       u8 vendor_specific_err;
> +       u32 state;
> +};
> +
> +static void dsm_status_err_print(struct pci_dev *pdev,
> +                                struct ssdleds_dsm_output *output)
> +{
> +       switch (output->status) {
> +       case 0:
> +               break;
> +       case 1:
> +               pci_dbg(pdev, "_DSM not supported\n");
> +               break;
> +       case 2:
> +               pci_dbg(pdev, "_DSM invalid input parameters\n");
> +               break;
> +       case 3:
> +               pci_dbg(pdev, "_DSM communication error\n");
> +               break;
> +       case 4:
> +               pci_dbg(pdev, "_DSM function-specific error 0x%x\n",
> +                       output->function_specific_err);
> +               break;
> +       case 5:
> +               pci_dbg(pdev, "_DSM vendor-specific error 0x%x\n",
> +                       output->vendor_specific_err);
> +               break;
> +       default:
> +               pci_dbg(pdev, "_DSM returned unknown status 0x%x\n",
> +                       output->status);
> +       }
> +}
> +
> +static int dsm_set(struct pci_dev *pdev, u32 value)
> +{
> +       acpi_handle handle;
> +       union acpi_object *out_obj, arg3[2];
> +       struct ssdleds_dsm_output *dsm_output;
> +
> +       handle = ACPI_HANDLE(&pdev->dev);
> +       if (!handle)
> +               return -ENODEV;
> +
> +       printk("dsm_set pdev %x:%x.%d value %x\n", pdev->bus->number, PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn), value);
> +
> +       arg3[0].type = ACPI_TYPE_PACKAGE;
> +       arg3[0].package.count = 1;
> +       arg3[0].package.elements = &arg3[1];
> +
> +       arg3[1].type = ACPI_TYPE_BUFFER;
> +       arg3[1].buffer.length = 4;
> +       arg3[1].buffer.pointer = (u8 *)&value;
> +
> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
> +                               1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
> +       if (!out_obj)
> +               return -EIO;
> +
> +       if (out_obj->buffer.length < 8) {
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +
> +       if (dsm_output->status != 0) {
> +               dsm_status_err_print(pdev, dsm_output);
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +       ACPI_FREE(out_obj);
> +       return 0;
> +}
> +
> +static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
> +{
> +       acpi_handle handle;
> +       union acpi_object *out_obj;
> +       struct ssdleds_dsm_output *dsm_output;
> +
> +       handle = ACPI_HANDLE(&pdev->dev);
> +       if (!handle)
> +               return -ENODEV;
> +
> +       printk("dsm_get pdev %x:%x.%d\n", pdev->bus->number, PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn));
> +
> +       out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +                                         dsm_func, NULL, ACPI_TYPE_BUFFER);
> +       if (!out_obj)
> +               return -EIO;
> +
> +       if (out_obj->buffer.length < 8) {
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +       if (dsm_output->status != 0) {
> +               dsm_status_err_print(pdev, dsm_output);
> +               ACPI_FREE(out_obj);
> +               return -EIO;
> +       }
> +
> +       *output = dsm_output->state;
> +       ACPI_FREE(out_obj);
> +       printk("...got %x\n", *output);
> +       return 0;
> +}
> +
> +static int get_supported_states_dsm(struct ssd_led_dev *sldev)
> +{
> +       return dsm_get(sldev->pdev, GET_SUPPORTED_STATES_DSM, &sldev->supported_states);
> +}
> +
> +static int get_current_states_dsm(struct ssd_led_dev *sldev, u32 *states)
> +{
> +       return dsm_get(sldev->pdev, GET_STATE_DSM, states);
> +}
> +
> +static int set_current_states_dsm(struct ssd_led_dev *sldev, u32 states)
> +{
> +       return dsm_set(sldev->pdev, states);
> +}
> +
> +static bool pdev_has_dsm(struct pci_dev *pdev)
> +{
> +       acpi_handle handle = ACPI_HANDLE(&pdev->dev);
> +
> +       if (!handle)
> +               return false;
> +
> +       return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +                             1 << GET_SUPPORTED_STATES_DSM ||
> +                             1 << GET_STATE_DSM ||
> +                             1 << SET_STATE_DSM);
> +}
> +
> +static struct drive_status_ops dsm_drive_status_ops = {
> +       .get_supported_states = get_supported_states_dsm,
> +       .get_current_states = get_current_states_dsm,
> +       .set_current_states = set_current_states_dsm,
> +};
> +
> +/*
> + * end of _DSM code
> + */
> +
> +static void ssd_leds_get_led(struct enclosure_device *edev,
> +                            struct enclosure_component *ecomp,
> +                            enum enclosure_component_led led)
> +{
> +       struct ssd_led_dev *sldev = ecomp->scratch;
> +       u32 states = 0;
> +
> +       sldev->ops->get_current_states(sldev, &states);
> +       ecomp->led[led] = !!(states & to_npem_state[led]) ?
> +               ENCLOSURE_SETTING_ENABLED : ENCLOSURE_SETTING_DISABLED;
> +}
> +
> +static int ssd_leds_set_led(struct enclosure_device *edev,
> +                        struct enclosure_component *ecomp,
> +                        enum enclosure_component_led led,
> +                        enum enclosure_component_setting val)
> +{
> +       struct ssd_led_dev *sldev = ecomp->scratch;
> +       u32 npem_state = to_npem_state[led], states;
> +       int rc;
> +
> +       if (val != ENCLOSURE_SETTING_ENABLED
> +           && val != ENCLOSURE_SETTING_DISABLED)
> +               return -EINVAL;
> +
> +       states = sldev->states & ~npem_state;
> +       states |= val == ENCLOSURE_SETTING_ENABLED ? npem_state : 0;
> +
> +       if ((states & sldev->supported_states) != states) {
> +               printk("can't set state %x\n", states);
> +               return -EINVAL;
> +       }
> +
> +       rc = sldev->ops->set_current_states(sldev, states);
> +       /*
> +        * save last written state so it doesn't have to be re-read via NPEM/
> +        * _DSM on the next write, not only because it is faster, but because
> +        * _DSM doesn't provide a way to know if the last write has completed
> +        */
> +       if (rc == 0)
> +               sldev->states = states;
> +       return rc;
> +}
> +
> +static struct enclosure_component_callbacks ssdleds_cb = {
> +       .get_led        = ssd_leds_get_led,
> +       .set_led        = ssd_leds_set_led,
> +};
> +
> +static struct ssd_led_dev *to_ssd_led_dev(struct pci_dev *pdev)
> +{
> +       struct ssd_led_dev *sldev;
> +
> +       list_for_each_entry(sldev, &ssd_led_dev_list, list)
> +               if (pdev == sldev->pdev)
> +                       return sldev;
> +       return NULL;
> +}
> +
> +static void remove_ssd_led_dev(struct ssd_led_dev *sldev)
> +{
> +       enclosure_unregister(sldev->edev);
> +       list_del(&sldev->list);
> +       kfree(sldev);
> +}
> +
> +static int add_ssd_led_dev(struct pci_dev *pdev,
> +                               const char *name,
> +                               struct drive_status_ops *ops)
> +{
> +       struct ssd_led_dev *sldev;
> +       struct enclosure_device *edev;
> +       struct enclosure_component *ecomp;
> +       int rc = 0;
> +
> +       mutex_lock(&drive_status_lock);
> +       if (ssd_leds_exiting)
> +               goto out_unlock;
> +
> +       sldev = kzalloc(sizeof(*sldev), GFP_KERNEL);
> +       if (!sldev) {
> +               rc = -ENOMEM;
> +               goto out_unlock;
> +       }
> +       sldev->pdev = pdev;
> +       sldev->ops = ops;
> +       sldev->states = 0;
> +       INIT_LIST_HEAD(&sldev->list);
> +       if (sldev->ops->get_supported_states(sldev) != 0)
> +               goto out_free;
> +
> +       edev = enclosure_register(&pdev->dev, name, 1, &ssdleds_cb);
> +       if (!edev)
> +               goto out_free;
> +
> +       ecomp = enclosure_component_alloc(edev, 0, ENCLOSURE_COMPONENT_DEVICE, "leds");
> +       if (IS_ERR(ecomp))
> +               goto out_unreg;
> +
> +       ecomp->type = ENCLOSURE_COMPONENT_ARRAY_DEVICE;
> +       rc = enclosure_component_register(ecomp);
> +       if (rc < 0)
> +               goto out_unreg;
> +
> +       ecomp->scratch = sldev;
> +       sldev->edev = edev;
> +       list_add_tail(&sldev->list, &ssd_led_dev_list);

Why not just store sldev in the in the auxdev driver-data. I don't see
a reason for ssd_led_dev_list to exist.

> +       goto out_unlock;
> +
> +out_unreg:
> +       enclosure_unregister(edev);
> +out_free:
> +       kfree(sldev);
> +out_unlock:
> +       mutex_unlock(&drive_status_lock);
> +       return rc;
> +}
> +
> +static int ssd_leds_probe(struct auxiliary_device *adev,
> +                         const struct auxiliary_device_id *id)
> +{
> +       struct pci_dev *pdev = to_pci_dev(adev->dev.parent);
> +
> +       if (pdev_has_dsm(pdev))

This is likely an argument for separate drivers for the NPEM and ACPI
_DSM case, because it should never be the case that the code gets this
far and this check returns false. If this is false the auxdev
shouldn't have been registered in the first instance.

> +               return add_ssd_led_dev(pdev, dev_name(&adev->dev), &dsm_drive_status_ops);
> +       return 0;
> +}
> +
> +static void ssd_leds_remove(struct auxiliary_device *adev)
> +{
> +       struct pci_dev *pdev = to_pci_dev(adev->dev.parent);
> +       struct ssd_led_dev *sldev;
> +
> +       mutex_lock(&drive_status_lock);
> +       sldev = to_ssd_led_dev(pdev);
> +       if (sldev)
> +               remove_ssd_led_dev(sldev);
> +out_unlock:
> +       mutex_unlock(&drive_status_lock);
> +}
> +
> +static const struct auxiliary_device_id ssd_leds_id_table[] = {
> +       { .name = "nvme.ssd_leds", },
> +//     { .name = "pcieport.ssd_leds", },
> +//     { .name = "cxl.ssd_leds", },
> +       {},
> +};
> +
> +MODULE_DEVICE_TABLE(auxiliary, ssd_leds_id_table);
> +
> +static struct auxiliary_driver ssd_leds_driver = {
> +       .name = "ssd_leds",
> +       .probe = ssd_leds_probe,
> +       .remove = ssd_leds_remove,
> +       .id_table = ssd_leds_id_table,
> +};
> +
> +static int __init ssd_leds_init(void)
> +{
> +       mutex_init(&drive_status_lock);
> +       INIT_LIST_HEAD(&ssd_led_dev_list);
> +
> +       to_npem_state[ENCLOSURE_LED_FAULT] = NPEM_STATE_FAILED;
> +       to_npem_state[ENCLOSURE_LED_ACTIVE] = 0;
> +       to_npem_state[ENCLOSURE_LED_LOCATE] = NPEM_STATE_LOCATE;
> +       to_npem_state[ENCLOSURE_LED_OK] = NPEM_STATE_OK;
> +       to_npem_state[ENCLOSURE_LED_REBUILD] = NPEM_STATE_REBUILD;
> +       to_npem_state[ENCLOSURE_LED_PRDFAIL] = NPEM_STATE_PFA;
> +       to_npem_state[ENCLOSURE_LED_HOTSPARE] = NPEM_STATE_HOTSPARE;
> +       to_npem_state[ENCLOSURE_LED_ICA] = NPEM_STATE_ICA;
> +       to_npem_state[ENCLOSURE_LED_IFA] = NPEM_STATE_IFA;
> +       to_npem_state[ENCLOSURE_LED_DISABLED] = NPEM_STATE_DISABLED;
> +
> +       auxiliary_driver_register(&ssd_leds_driver);
> +       return 0;
> +}
> +
> +static void __exit ssd_leds_exit(void)
> +{
> +       struct ssd_led_dev *sldev, *temp;
> +
> +       mutex_lock(&drive_status_lock);
> +       ssd_leds_exiting = 1;
> +       list_for_each_entry_safe(sldev, temp, &ssd_led_dev_list, list)
> +               remove_ssd_led_dev(sldev);
> +       mutex_unlock(&drive_status_lock);
> +       auxiliary_driver_unregister(&ssd_leds_driver);
> +}
> +
> +module_init(ssd_leds_init);
> +module_exit(ssd_leds_exit);
> +
> +MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
> +MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
> +MODULE_LICENSE("GPL");
James Bottomley Nov. 7, 2021, 2:40 p.m. UTC | #13
On Fri, 2021-11-05 at 19:52 -0700, Dan Williams wrote:
> [ add James and Martin in case they see anything here that collides
> with the SES driver ]

It's a bit difficult to tell among all the quoting.  However, I will
say that if you want to play with drivers/misc/enclosure.c you need at
least to cc the maintainer and preferably linux-scsi because they're
currently the only consumer ... and the enterprise does a lot with ses.

James
Krzysztof Wilczyński Nov. 12, 2021, 12:56 a.m. UTC | #14
Hi Stuart,

[...]
> I've reworked the code so it is an auxiliary driver (to nvme, and
> hopefully cxl and pcieport, but I haven't added that yet), and so it
> registers as an enclosure (instead of using the LED subsystem).

Would you be able to post this new reworded driver as either a new
version or a completely new series, or even as an RFC?  It would help
to get a fresh perspective and also make it easier to review it.

	Krzysztof
Bjorn Helgaas Nov. 25, 2021, 10:13 p.m. UTC | #15
On Fri, Aug 13, 2021 at 05:36:53PM -0400, Stuart Hayes wrote:
> This patch adds support for the PCIe SSD Status LED Management
> interface, as described in the "_DSM Additions for PCIe SSD Status LED
> Management" ECN to the PCI Firmware Specification revision 3.2.

I think r3.3 has been released, so we can cite that now.

> It will add (led_classdev) LEDs to each PCIe device that has a supported
> _DSM interface (one off/on LED per supported state). Both PCIe storage
> devices, and the ports to which they are connected, can support this
> interface.

To be OCD, we should tighten up the "ports to which they are
connected" part (see details below).

> + * drive_status_dev->dev could be the drive itself or its PCIe port

I assume you mean "drive_status_dev->pdev" (not "->dev")?

"... drive itself or its PCIe port" is ambiguous because switches and
endpoints both have Ports.  It sounds like this refers to either the
endpoint or the switch downstream port leading to it, which would
match the PCI Firmware spec language about the _DSM being "under the
ACPI object representing the embedded PCIe device/function or under
the ACPI PCIe description representing the add-in PCI Express slot."

> +struct drive_status_dev {
> +	struct list_head list;
> +	/* PCI device that has the LED controls */
> +	struct pci_dev *pdev;
> +	/* _DSM (or NPEM) LED ops */
> +	struct drive_status_led_ops *ops;
> +	/* currently active states */
> +	u32 states;
> +	int num_leds;
> +	struct drive_status_state_led leds[];
> +};

I think this would be easier to read if you can fit the comments to
the right on the same line, e.g.,

        struct pci_dev *pdev;           /* dev with controls */

> +static void dsm_status_err_print(struct pci_dev *pdev,
> +				 struct ssdleds_dsm_output *output)
> +{
> +	switch (output->status) {
> +	case 0:
> +		break;
> +	case 1:
> +		pci_dbg(pdev, "_DSM not supported\n");

pci_dbg() often goes nowhere.  Seems like these might be pci_info()?

> +static int dsm_set(struct pci_dev *pdev, u32 value)
> +{
> +	acpi_handle handle;
> +	union acpi_object *out_obj, arg3[2];
> +	struct ssdleds_dsm_output *dsm_output;
> +
> +	handle = ACPI_HANDLE(&pdev->dev);
> +	if (!handle)
> +		return -ENODEV;
> +
> +	arg3[0].type = ACPI_TYPE_PACKAGE;
> +	arg3[0].package.count = 1;
> +	arg3[0].package.elements = &arg3[1];
> +
> +	arg3[1].type = ACPI_TYPE_BUFFER;
> +	arg3[1].buffer.length = 4;
> +	arg3[1].buffer.pointer = (u8 *)&value;
> +
> +	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
> +				1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
> +	if (!out_obj)
> +		return -EIO;
> +
> +	if (out_obj->buffer.length < 8) {
> +		ACPI_FREE(out_obj);
> +		return -EIO;
> +	}
> +
> +	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +
> +	if (dsm_output->status != 0) {
> +		dsm_status_err_print(pdev, dsm_output);
> +		ACPI_FREE(out_obj);
> +		return -EIO;
> +	}
> +	ACPI_FREE(out_obj);
> +	return 0;
> +}
> +
> +static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
> +{
> +	acpi_handle handle;
> +	union acpi_object *out_obj;
> +	struct ssdleds_dsm_output *dsm_output;
> +
> +	handle = ACPI_HANDLE(&pdev->dev);
> +	if (!handle)
> +		return -ENODEV;
> +
> +	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +					  dsm_func, NULL, ACPI_TYPE_BUFFER);
> +	if (!out_obj)
> +		return -EIO;
> +
> +	if (out_obj->buffer.length < 8) {
> +		ACPI_FREE(out_obj);
> +		return -EIO;
> +	}
> +
> +	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
> +	if (dsm_output->status != 0) {
> +		dsm_status_err_print(pdev, dsm_output);
> +		ACPI_FREE(out_obj);
> +		return -EIO;
> +	}
> +
> +	*output = dsm_output->state;
> +	ACPI_FREE(out_obj);
> +	return 0;
> +}
> +
> +static int get_supported_states_dsm(struct pci_dev *pdev, u32 *states)
> +{
> +	return dsm_get(pdev, GET_SUPPORTED_STATES_DSM, states);
> +}
> +
> +static int get_current_states_dsm(struct pci_dev *pdev, u32 *states)
> +{
> +	return dsm_get(pdev, GET_STATE_DSM, states);
> +}
> +
> +static int set_current_states_dsm(struct pci_dev *pdev, u32 states)
> +{
> +	return dsm_set(pdev, states);
> +}
> +
> +static bool pdev_has_dsm(struct pci_dev *pdev)
> +{
> +	acpi_handle handle;
> +
> +	handle = ACPI_HANDLE(&pdev->dev);
> +	if (!handle)
> +		return false;
> +
> +	return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
> +			      1 << GET_SUPPORTED_STATES_DSM ||
> +			      1 << GET_STATE_DSM ||
> +			      1 << SET_STATE_DSM);
> +}
> +
> +struct drive_status_led_ops dsm_drive_status_led_ops = {
> +	.get_supported_states = get_supported_states_dsm,
> +	.get_current_states = get_current_states_dsm,
> +	.set_current_states = set_current_states_dsm,
> +};
> +
> +/*
> + * code not specific to method (_DSM/NPEM)
> + */
> +
> +static int set_brightness(struct led_classdev *led_cdev,
> +				       enum led_brightness brightness)
> +{
> +	struct drive_status_state_led *led;
> +	int err;
> +
> +	led = container_of(led_cdev, struct drive_status_state_led, cdev);
> +
> +	if (brightness == LED_OFF)
> +		clear_bit(led->bit, (unsigned long *)&(led->dsdev->states));
> +	else
> +		set_bit(led->bit, (unsigned long *)&(led->dsdev->states));
> +	err = led->dsdev->ops->set_current_states(led->dsdev->pdev,
> +						  led->dsdev->states);
> +	if (err < 0)
> +		return err;
> +	return 0;

Looks currently equivalent to simply "return err" (or
"return led->dsdev->ops->set_current_states(...)").

Is there a case where the ops might return something positive?

> +}
> +
> +static enum led_brightness get_brightness(struct led_classdev *led_cdev)
> +{
> +	struct drive_status_state_led *led;
> +
> +	led = container_of(led_cdev, struct drive_status_state_led, cdev);
> +	return test_bit(led->bit, (unsigned long *)&led->dsdev->states)
> +		? LED_ON : LED_OFF;
> +}
> +
> +static struct drive_status_dev *to_drive_status_dev(struct pci_dev *pdev)
> +{
> +	struct drive_status_dev *dsdev;
> +
> +	mutex_lock(&drive_status_dev_list_lock);
> +	list_for_each_entry(dsdev, &drive_status_dev_list, list)
> +		if (pdev == dsdev->pdev) {
> +			mutex_unlock(&drive_status_dev_list_lock);
> +			return dsdev;
> +		}

Even though the four-line body is technically a single statement, I
think braces around those lines would improve readability.

> +	mutex_unlock(&drive_status_dev_list_lock);
> +	return NULL;
> +}
> +
> +static void remove_drive_status_dev(struct drive_status_dev *dsdev)
> +{
> +	if (dsdev) {
> +		int i;

  if (!dsdev)
    return;

so you can unindent the usual path.

> +
> +		mutex_lock(&drive_status_dev_list_lock);
> +		list_del(&dsdev->list);
> +		mutex_unlock(&drive_status_dev_list_lock);
> +		for (i = 0; i < dsdev->num_leds; i++)
> +			led_classdev_unregister(&dsdev->leds[i].cdev);
> +		kfree(dsdev);
> +	}
> +}
> +
> +static void add_drive_status_dev(struct pci_dev *pdev,
> +				 struct drive_status_led_ops *ops)
> +{
> +	u32 supported;
> +	int ret, num_leds, i;
> +	struct drive_status_dev *dsdev;
> +	char name[LED_MAX_NAME_SIZE];
> +	struct drive_status_state_led *led;
> +
> +	if (to_drive_status_dev(pdev))
> +		/*
> +		 * leds have already been added for this dev

s/leds/LEDs/ to match other usage.

> +		 */
> +		return;
> +
> +	if (ops->get_supported_states(pdev, &supported) < 0)
> +		return;
> +	num_leds = hweight32(supported);
> +	if (num_leds == 0)
> +		return;
> +
> +	dsdev = kzalloc(struct_size(dsdev, leds, num_leds), GFP_KERNEL);
> +	if (!dsdev)
> +		return;
> +
> +	dsdev->num_leds = 0;
> +	dsdev->pdev = pdev;
> +	dsdev->ops = ops;
> +	dsdev->states = 0;
> +	if (ops->set_current_states(pdev, dsdev->states)) {
> +		kfree(dsdev);
> +		return;
> +	}
> +	INIT_LIST_HEAD(&dsdev->list);
> +	/*
> +	 * add LEDs only for supported states
> +	 */
> +	for (i = 0; i < ARRAY_SIZE(led_states); i++) {
> +		if (!test_bit(led_states[i].bit, (unsigned long *)&supported))
> +			continue;
> +
> +		led = &dsdev->leds[dsdev->num_leds];
> +		led->dsdev = dsdev;
> +		led->bit = led_states[i].bit;
> +
> +		snprintf(name, sizeof(name), "%s::%s",
> +			 pci_name(pdev), led_states[i].name);
> +		led->cdev.name = name;
> +		led->cdev.max_brightness = LED_ON;
> +		led->cdev.brightness_set_blocking = set_brightness;
> +		led->cdev.brightness_get = get_brightness;
> +		ret = 0;
> +		ret = led_classdev_register(&pdev->dev, &led->cdev);
> +		if (ret) {
> +			pr_warn("Failed to register LEDs for %s\n", pci_name(pdev));

pci_warn().  Maybe include the led_states[i].name?

> +			remove_drive_status_dev(dsdev);
> +			return;
> +		}
> +		dsdev->num_leds++;
> +	}
> +
> +	mutex_lock(&drive_status_dev_list_lock);
> +	list_add_tail(&dsdev->list, &drive_status_dev_list);
> +	mutex_unlock(&drive_status_dev_list_lock);
> +}
> +
> +/*
> + * code specific to PCIe devices
> + */
> +static void probe_pdev(struct pci_dev *pdev)
> +{
> +	/*
> +	 * This is only supported on PCIe storage devices and PCIe ports
> +	 */
> +	if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
> +	    pdev->class != PCI_CLASS_BRIDGE_PCI)

I don't see this restriction in the spec.  Did I miss it?

> +		return;
> +	if (pdev_has_dsm(pdev))
> +		add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
> +}
> +
> +static int ssd_leds_pci_bus_notifier_cb(struct notifier_block *nb,
> +					   unsigned long action, void *data)
> +{
> +	struct pci_dev *pdev = to_pci_dev(data);
> +
> +	if (action == BUS_NOTIFY_ADD_DEVICE)
> +		probe_pdev(pdev);
> +	else if (action == BUS_NOTIFY_DEL_DEVICE)
> +		remove_drive_status_dev(to_drive_status_dev(pdev));
> +	return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block ssd_leds_pci_bus_nb = {
> +	.notifier_call = ssd_leds_pci_bus_notifier_cb,
> +	.priority = INT_MIN,
> +};
> +
> +static void initial_scan_for_leds(void)
> +{
> +	struct pci_dev *pdev = NULL;
> +
> +	for_each_pci_dev(pdev)
> +		probe_pdev(pdev);
> +}
> +
> +static int __init ssd_leds_init(void)
> +{
> +	mutex_init(&drive_status_dev_list_lock);
> +	INIT_LIST_HEAD(&drive_status_dev_list);
> +
> +	bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
> +	initial_scan_for_leds();

This is the only way to deal with (a) making this a module that can be
loaded later and (b) handling hot-added devices, so I see why this is
here.  But it's ugly.  IMO, for_each_pci_dev() is a symptom of
something that should have been done during enumeration, but wasn't.

> +	return 0;
> +}
> +
> +static void __exit ssd_leds_exit(void)
> +{
> +	struct drive_status_dev *dsdev, *temp;
> +
> +	bus_unregister_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
> +	list_for_each_entry_safe(dsdev, temp, &drive_status_dev_list, list)
> +		remove_drive_status_dev(dsdev);
> +}
> +
> +module_init(ssd_leds_init);
> +module_exit(ssd_leds_exit);
> +
> +MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
> +MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
> +MODULE_LICENSE("GPL");
> -- 
> 2.27.0
>
diff mbox series

Patch

diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 45a2ef702b45..b738d473209f 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -142,3 +142,14 @@  config PCIE_EDR
 	  the PCI Firmware Specification r3.2.  Enable this if you want to
 	  support hybrid DPC model which uses both firmware and OS to
 	  implement DPC.
+
+config PCIE_SSD_LEDS
+	tristate "PCIe SSD status LED support"
+	depends on ACPI && LEDS_CLASS
+	help
+	  Driver for PCIe SSD status LED management as described in a PCI
+	  Firmware Specification, Revision 3.2 ECN.
+
+	  When enabled, LED interfaces will be created for supported drive
+	  states for each PCIe device that has the ACPI _DSM method described
+	  in the referenced specification.
diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
index b2980db88cc0..fbcb8c2d1dc1 100644
--- a/drivers/pci/pcie/Makefile
+++ b/drivers/pci/pcie/Makefile
@@ -13,3 +13,4 @@  obj-$(CONFIG_PCIE_PME)		+= pme.o
 obj-$(CONFIG_PCIE_DPC)		+= dpc.o
 obj-$(CONFIG_PCIE_PTM)		+= ptm.o
 obj-$(CONFIG_PCIE_EDR)		+= edr.o
+obj-$(CONFIG_PCIE_SSD_LEDS)	+= ssd-leds.o
diff --git a/drivers/pci/pcie/ssd-leds.c b/drivers/pci/pcie/ssd-leds.c
new file mode 100644
index 000000000000..cacb77e5da2d
--- /dev/null
+++ b/drivers/pci/pcie/ssd-leds.c
@@ -0,0 +1,419 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Module to provide LED interfaces for PCIe SSD status LED states, as
+ * defined in the "_DSM additions for PCIe SSD Status LED Management" ECN
+ * to the PCI Firmware Specification Revision 3.2, dated 12 February 2020.
+ *
+ * The "_DSM..." spec is functionally similar to Native PCIe Enclosure
+ * Management, but uses a _DSM ACPI method rather than a PCIe extended
+ * capability.
+ *
+ * Copyright (c) 2021 Dell Inc.
+ *
+ * TODO: Add NPEM support
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <uapi/linux/uleds.h>
+
+#define DRIVER_NAME	"pcie-ssd-leds"
+#define DRIVER_VERSION	"v1.0"
+
+struct led_state {
+	char *name;
+	int bit;
+};
+
+static struct led_state led_states[] = {
+	{ .name = "ok",		.bit = 2 },
+	{ .name = "locate",	.bit = 3 },
+	{ .name = "failed",	.bit = 4 },
+	{ .name = "rebuild",	.bit = 5 },
+	{ .name = "pfa",	.bit = 6 },
+	{ .name = "hotspare",	.bit = 7 },
+	{ .name = "ica",	.bit = 8 },
+	{ .name = "ifa",	.bit = 9 },
+	{ .name = "invalid",	.bit = 10 },
+	{ .name = "disabled",	.bit = 11 },
+};
+
+struct drive_status_led_ops {
+	int (*get_supported_states)(struct pci_dev *pdev, u32 *states);
+	int (*get_current_states)(struct pci_dev *pdev, u32 *states);
+	int (*set_current_states)(struct pci_dev *pdev, u32 states);
+};
+
+struct drive_status_state_led {
+	struct led_classdev cdev;
+	struct drive_status_dev *dsdev;
+	int bit;
+};
+
+/*
+ * drive_status_dev->dev could be the drive itself or its PCIe port
+ */
+struct drive_status_dev {
+	struct list_head list;
+	/* PCI device that has the LED controls */
+	struct pci_dev *pdev;
+	/* _DSM (or NPEM) LED ops */
+	struct drive_status_led_ops *ops;
+	/* currently active states */
+	u32 states;
+	int num_leds;
+	struct drive_status_state_led leds[];
+};
+
+struct mutex drive_status_dev_list_lock;
+struct list_head drive_status_dev_list;
+
+/*
+ * _DSM LED control
+ */
+const guid_t pcie_ssd_leds_dsm_guid =
+	GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
+		  0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
+
+#define GET_SUPPORTED_STATES_DSM	0x01
+#define GET_STATE_DSM			0x02
+#define SET_STATE_DSM			0x03
+
+struct ssdleds_dsm_output {
+	u16 status;
+	u8 function_specific_err;
+	u8 vendor_specific_err;
+	u32 state;
+};
+
+static void dsm_status_err_print(struct pci_dev *pdev,
+				 struct ssdleds_dsm_output *output)
+{
+	switch (output->status) {
+	case 0:
+		break;
+	case 1:
+		pci_dbg(pdev, "_DSM not supported\n");
+		break;
+	case 2:
+		pci_dbg(pdev, "_DSM invalid input parameters\n");
+		break;
+	case 3:
+		pci_dbg(pdev, "_DSM communication error\n");
+		break;
+	case 4:
+		pci_dbg(pdev, "_DSM function-specific error 0x%x\n",
+			output->function_specific_err);
+		break;
+	case 5:
+		pci_dbg(pdev, "_DSM vendor-specific error 0x%x\n",
+			output->vendor_specific_err);
+		break;
+	default:
+		pci_dbg(pdev, "_DSM returned unknown status 0x%x\n",
+			output->status);
+	}
+}
+
+static int dsm_set(struct pci_dev *pdev, u32 value)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj, arg3[2];
+	struct ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return -ENODEV;
+
+	arg3[0].type = ACPI_TYPE_PACKAGE;
+	arg3[0].package.count = 1;
+	arg3[0].package.elements = &arg3[1];
+
+	arg3[1].type = ACPI_TYPE_BUFFER;
+	arg3[1].buffer.length = 4;
+	arg3[1].buffer.pointer = (u8 *)&value;
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid,
+				1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
+
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(pdev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+	ACPI_FREE(out_obj);
+	return 0;
+}
+
+static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *output)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj;
+	struct ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return -ENODEV;
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssd_leds_dsm_guid, 0x1,
+					  dsm_func, NULL, ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct ssdleds_dsm_output *)out_obj->buffer.pointer;
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(pdev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	*output = dsm_output->state;
+	ACPI_FREE(out_obj);
+	return 0;
+}
+
+static int get_supported_states_dsm(struct pci_dev *pdev, u32 *states)
+{
+	return dsm_get(pdev, GET_SUPPORTED_STATES_DSM, states);
+}
+
+static int get_current_states_dsm(struct pci_dev *pdev, u32 *states)
+{
+	return dsm_get(pdev, GET_STATE_DSM, states);
+}
+
+static int set_current_states_dsm(struct pci_dev *pdev, u32 states)
+{
+	return dsm_set(pdev, states);
+}
+
+static bool pdev_has_dsm(struct pci_dev *pdev)
+{
+	acpi_handle handle;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return false;
+
+	return acpi_check_dsm(handle, &pcie_ssd_leds_dsm_guid, 0x1,
+			      1 << GET_SUPPORTED_STATES_DSM ||
+			      1 << GET_STATE_DSM ||
+			      1 << SET_STATE_DSM);
+}
+
+struct drive_status_led_ops dsm_drive_status_led_ops = {
+	.get_supported_states = get_supported_states_dsm,
+	.get_current_states = get_current_states_dsm,
+	.set_current_states = set_current_states_dsm,
+};
+
+/*
+ * code not specific to method (_DSM/NPEM)
+ */
+
+static int set_brightness(struct led_classdev *led_cdev,
+				       enum led_brightness brightness)
+{
+	struct drive_status_state_led *led;
+	int err;
+
+	led = container_of(led_cdev, struct drive_status_state_led, cdev);
+
+	if (brightness == LED_OFF)
+		clear_bit(led->bit, (unsigned long *)&(led->dsdev->states));
+	else
+		set_bit(led->bit, (unsigned long *)&(led->dsdev->states));
+	err = led->dsdev->ops->set_current_states(led->dsdev->pdev,
+						  led->dsdev->states);
+	if (err < 0)
+		return err;
+	return 0;
+}
+
+static enum led_brightness get_brightness(struct led_classdev *led_cdev)
+{
+	struct drive_status_state_led *led;
+
+	led = container_of(led_cdev, struct drive_status_state_led, cdev);
+	return test_bit(led->bit, (unsigned long *)&led->dsdev->states)
+		? LED_ON : LED_OFF;
+}
+
+static struct drive_status_dev *to_drive_status_dev(struct pci_dev *pdev)
+{
+	struct drive_status_dev *dsdev;
+
+	mutex_lock(&drive_status_dev_list_lock);
+	list_for_each_entry(dsdev, &drive_status_dev_list, list)
+		if (pdev == dsdev->pdev) {
+			mutex_unlock(&drive_status_dev_list_lock);
+			return dsdev;
+		}
+	mutex_unlock(&drive_status_dev_list_lock);
+	return NULL;
+}
+
+static void remove_drive_status_dev(struct drive_status_dev *dsdev)
+{
+	if (dsdev) {
+		int i;
+
+		mutex_lock(&drive_status_dev_list_lock);
+		list_del(&dsdev->list);
+		mutex_unlock(&drive_status_dev_list_lock);
+		for (i = 0; i < dsdev->num_leds; i++)
+			led_classdev_unregister(&dsdev->leds[i].cdev);
+		kfree(dsdev);
+	}
+}
+
+static void add_drive_status_dev(struct pci_dev *pdev,
+				 struct drive_status_led_ops *ops)
+{
+	u32 supported;
+	int ret, num_leds, i;
+	struct drive_status_dev *dsdev;
+	char name[LED_MAX_NAME_SIZE];
+	struct drive_status_state_led *led;
+
+	if (to_drive_status_dev(pdev))
+		/*
+		 * leds have already been added for this dev
+		 */
+		return;
+
+	if (ops->get_supported_states(pdev, &supported) < 0)
+		return;
+	num_leds = hweight32(supported);
+	if (num_leds == 0)
+		return;
+
+	dsdev = kzalloc(struct_size(dsdev, leds, num_leds), GFP_KERNEL);
+	if (!dsdev)
+		return;
+
+	dsdev->num_leds = 0;
+	dsdev->pdev = pdev;
+	dsdev->ops = ops;
+	dsdev->states = 0;
+	if (ops->set_current_states(pdev, dsdev->states)) {
+		kfree(dsdev);
+		return;
+	}
+	INIT_LIST_HEAD(&dsdev->list);
+	/*
+	 * add LEDs only for supported states
+	 */
+	for (i = 0; i < ARRAY_SIZE(led_states); i++) {
+		if (!test_bit(led_states[i].bit, (unsigned long *)&supported))
+			continue;
+
+		led = &dsdev->leds[dsdev->num_leds];
+		led->dsdev = dsdev;
+		led->bit = led_states[i].bit;
+
+		snprintf(name, sizeof(name), "%s::%s",
+			 pci_name(pdev), led_states[i].name);
+		led->cdev.name = name;
+		led->cdev.max_brightness = LED_ON;
+		led->cdev.brightness_set_blocking = set_brightness;
+		led->cdev.brightness_get = get_brightness;
+		ret = 0;
+		ret = led_classdev_register(&pdev->dev, &led->cdev);
+		if (ret) {
+			pr_warn("Failed to register LEDs for %s\n", pci_name(pdev));
+			remove_drive_status_dev(dsdev);
+			return;
+		}
+		dsdev->num_leds++;
+	}
+
+	mutex_lock(&drive_status_dev_list_lock);
+	list_add_tail(&dsdev->list, &drive_status_dev_list);
+	mutex_unlock(&drive_status_dev_list_lock);
+}
+
+/*
+ * code specific to PCIe devices
+ */
+static void probe_pdev(struct pci_dev *pdev)
+{
+	/*
+	 * This is only supported on PCIe storage devices and PCIe ports
+	 */
+	if (pdev->class != PCI_CLASS_STORAGE_EXPRESS &&
+	    pdev->class != PCI_CLASS_BRIDGE_PCI)
+		return;
+	if (pdev_has_dsm(pdev))
+		add_drive_status_dev(pdev, &dsm_drive_status_led_ops);
+}
+
+static int ssd_leds_pci_bus_notifier_cb(struct notifier_block *nb,
+					   unsigned long action, void *data)
+{
+	struct pci_dev *pdev = to_pci_dev(data);
+
+	if (action == BUS_NOTIFY_ADD_DEVICE)
+		probe_pdev(pdev);
+	else if (action == BUS_NOTIFY_DEL_DEVICE)
+		remove_drive_status_dev(to_drive_status_dev(pdev));
+	return NOTIFY_DONE;
+}
+
+static struct notifier_block ssd_leds_pci_bus_nb = {
+	.notifier_call = ssd_leds_pci_bus_notifier_cb,
+	.priority = INT_MIN,
+};
+
+static void initial_scan_for_leds(void)
+{
+	struct pci_dev *pdev = NULL;
+
+	for_each_pci_dev(pdev)
+		probe_pdev(pdev);
+}
+
+static int __init ssd_leds_init(void)
+{
+	mutex_init(&drive_status_dev_list_lock);
+	INIT_LIST_HEAD(&drive_status_dev_list);
+
+	bus_register_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
+	initial_scan_for_leds();
+	return 0;
+}
+
+static void __exit ssd_leds_exit(void)
+{
+	struct drive_status_dev *dsdev, *temp;
+
+	bus_unregister_notifier(&pci_bus_type, &ssd_leds_pci_bus_nb);
+	list_for_each_entry_safe(dsdev, temp, &drive_status_dev_list, list)
+		remove_drive_status_dev(dsdev);
+}
+
+module_init(ssd_leds_init);
+module_exit(ssd_leds_exit);
+
+MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@gmail.com>");
+MODULE_DESCRIPTION("Support for PCIe SSD Status LEDs");
+MODULE_LICENSE("GPL");