control: PCIe floors action

This action sets the PCIe floor index by looking up the highest floor
index of all the powered on PCIe cards that are recognized in the PCIe
card JSON files.  If a PCIe card has its own temperature sensor then it
doesn't provide a floor index.  If a card isn't recognized then it's
just ignored as it isn't considered a hot card.

The class documentation contains additional details on how the action
behaves.  It's JSON name is 'pcie_card_floors'.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I474916773476a6232d479119acb4ac2989909cb3
diff --git a/control/Makefile.am b/control/Makefile.am
index 3cd2c0a..4f68e80 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -88,6 +88,7 @@
 	json/actions/set_parameter_from_group_max.cpp \
 	json/actions/count_state_floor.cpp \
 	json/actions/get_managed_objects.cpp \
+	json/actions/pcie_card_floors.cpp \
 	json/utils/flight_recorder.cpp \
 	json/utils/modifier.cpp \
 	json/utils/pcie_card_metadata.cpp
diff --git a/control/json/actions/pcie_card_floors.cpp b/control/json/actions/pcie_card_floors.cpp
new file mode 100644
index 0000000..512c678
--- /dev/null
+++ b/control/json/actions/pcie_card_floors.cpp
@@ -0,0 +1,271 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "pcie_card_floors.hpp"
+
+#include "../manager.hpp"
+#include "json_config.hpp"
+#include "sdbusplus.hpp"
+#include "sdeventplus.hpp"
+
+namespace phosphor::fan::control::json
+{
+
+constexpr auto floorIndexParam = "pcie_floor_index";
+constexpr auto pcieDeviceIface =
+    "xyz.openbmc_project.Inventory.Item.PCIeDevice";
+constexpr auto powerStateIface =
+    "xyz.openbmc_project.State.Decorator.PowerState";
+constexpr auto deviceIDProp = "Function0DeviceId";
+constexpr auto vendorIDProp = "Function0VendorId";
+constexpr auto subsystemIDProp = "Function0SubsystemId";
+constexpr auto subsystemVendorIDProp = "Function0SubsystemVendorId";
+
+PCIeCardFloors::PCIeCardFloors(const json& jsonObj,
+                               const std::vector<Group>& groups) :
+    ActionBase(jsonObj, groups)
+{
+    loadCardJSON(jsonObj);
+}
+
+void PCIeCardFloors::run(Zone& zone)
+{
+    if (_settleTimer)
+    {
+        _settleTimer->setEnabled(false);
+    }
+    else
+    {
+        _settleTimer =
+            std::make_unique<Timer>(util::SDEventPlus::getEvent(),
+                                    [&zone, this](Timer&) { execute(zone); });
+    }
+    _settleTimer->restartOnce(_settleTime);
+}
+
+void PCIeCardFloors::execute(Zone& zone)
+{
+    size_t hotCards = 0;
+    size_t numTempSensorCards = 0;
+    size_t uninterestingCards = 0;
+    int32_t floorIndex = -1;
+
+    for (const auto& group : _groups)
+    {
+        if (group.getInterface() != powerStateIface)
+        {
+            log<level::DEBUG>(
+                fmt::format("Wrong interface {} in PCIe card floor group",
+                            group.getInterface())
+                    .c_str());
+            continue;
+        }
+
+        for (const auto& slotPath : group.getMembers())
+        {
+            PropertyVariantType powerState;
+
+            try
+            {
+                powerState = Manager::getObjValueVariant(
+                    slotPath, group.getInterface(), group.getProperty());
+            }
+            catch (const std::out_of_range& oore)
+            {
+                log<level::ERR>(
+                    fmt::format("Could not get power state for {}", slotPath)
+                        .c_str());
+                continue;
+            }
+
+            if (std::get<std::string>(powerState) !=
+                "xyz.openbmc_project.State.Decorator.PowerState.State.On")
+            {
+                continue;
+            }
+
+            auto floorIndexOrTempSensor = getFloorIndexFromSlot(slotPath);
+            if (floorIndexOrTempSensor)
+            {
+                if (std::holds_alternative<int32_t>(*floorIndexOrTempSensor))
+                {
+                    hotCards++;
+                    floorIndex = std::max(
+                        floorIndex, std::get<int32_t>(*floorIndexOrTempSensor));
+                }
+                else
+                {
+                    numTempSensorCards++;
+                }
+            }
+            else
+            {
+                uninterestingCards++;
+            }
+        }
+    }
+
+    auto status = fmt::format(
+        "Found {} hot cards, {} with temp sensors, {} uninteresting", hotCards,
+        numTempSensorCards, uninterestingCards);
+    if (status != _lastStatus)
+    {
+        record(status);
+        _lastStatus = status;
+    }
+
+    int32_t origIndex = -1;
+    auto origIndexVariant = Manager::getParameter(floorIndexParam);
+    if (origIndexVariant)
+    {
+        origIndex = std::get<int32_t>(*origIndexVariant);
+    }
+
+    if (floorIndex != -1)
+    {
+        if (origIndex != floorIndex)
+        {
+            record(fmt::format("Setting {} parameter to {}", floorIndexParam,
+                               floorIndex));
+            Manager::setParameter(floorIndexParam, floorIndex);
+        }
+    }
+    else if (origIndexVariant)
+    {
+        record(fmt::format("Removing parameter {}", floorIndexParam));
+        Manager::setParameter(floorIndexParam, std::nullopt);
+    }
+}
+
+void PCIeCardFloors::loadCardJSON(const json& jsonObj)
+{
+    std::string baseConfigFile;
+    bool useConfigSpecificFiles = false;
+
+    if (jsonObj.contains("settle_time"))
+    {
+        _settleTime =
+            std::chrono::seconds(jsonObj.at("settle_time").get<size_t>());
+    }
+
+    if (jsonObj.contains("use_config_specific_files"))
+    {
+        useConfigSpecificFiles =
+            jsonObj.at("use_config_specific_files").get<bool>();
+    }
+
+    std::vector<std::string> names;
+    if (useConfigSpecificFiles)
+    {
+        names = phosphor::fan::JsonConfig::getCompatValues();
+    }
+
+    _cardMetadata = std::make_unique<PCIeCardMetadata>(names);
+}
+
+uint16_t PCIeCardFloors::getPCIeDeviceProperty(const std::string& objectPath,
+                                               const std::string& propertyName)
+{
+    PropertyVariantType variantValue;
+    uint16_t value{};
+
+    try
+    {
+        variantValue = Manager::getObjValueVariant(objectPath, pcieDeviceIface,
+                                                   propertyName);
+    }
+    catch (const std::out_of_range& oore)
+    {
+        log<level::ERR>(
+            fmt::format(
+                "{}: Could not get PCIeDevice property {} {} from cache ",
+                ActionBase::getName(), objectPath, propertyName)
+                .c_str());
+        throw;
+    }
+
+    try
+    {
+        value = std::stoul(std::get<std::string>(variantValue), nullptr, 0);
+        return value;
+    }
+    catch (const std::invalid_argument& e)
+    {
+        log<level::INFO>(
+            fmt::format("{}: {} has invalid PCIeDevice property {} value: {}",
+                        ActionBase::getName(), objectPath, propertyName,
+                        std::get<std::string>(variantValue))
+                .c_str());
+        throw;
+    }
+}
+
+std::optional<std::variant<int32_t, bool>>
+    PCIeCardFloors::getFloorIndexFromSlot(const std::string& slotPath)
+{
+    const auto& card = getCardFromSlot(slotPath);
+
+    try
+    {
+        auto deviceID = getPCIeDeviceProperty(card, deviceIDProp);
+        auto vendorID = getPCIeDeviceProperty(card, vendorIDProp);
+        auto subsystemID = getPCIeDeviceProperty(card, subsystemIDProp);
+        auto subsystemVendorID =
+            getPCIeDeviceProperty(card, subsystemVendorIDProp);
+
+        return _cardMetadata->lookup(deviceID, vendorID, subsystemID,
+                                     subsystemVendorID);
+    }
+    catch (const std::exception& e)
+    {}
+
+    return std::nullopt;
+}
+
+const std::string& PCIeCardFloors::getCardFromSlot(const std::string& slotPath)
+{
+    auto cardIt = _cards.find(slotPath);
+
+    if (cardIt != _cards.end())
+    {
+        return cardIt->second;
+    }
+
+    // Just the first time, find all the PCIeDevice objects
+    if (_pcieDevices.empty())
+    {
+        _pcieDevices = util::SDBusPlus::getSubTreePaths(
+            util::SDBusPlus::getBus(), "/", pcieDeviceIface, 0);
+    }
+
+    // Find the card that plugs in this slot based on if the
+    // slot is part of the path, like slotA/cardA
+    auto it = std::find_if(
+        _pcieDevices.begin(), _pcieDevices.end(), [slotPath](const auto& path) {
+            return path.find(slotPath + '/') != std::string::npos;
+        });
+
+    if (it == _pcieDevices.end())
+    {
+        throw std::runtime_error(fmt::format(
+            "Could not find PCIe card object path for slot {}", slotPath));
+    }
+
+    _cards.emplace(slotPath, *it);
+
+    return _cards.at(slotPath);
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/actions/pcie_card_floors.hpp b/control/json/actions/pcie_card_floors.hpp
new file mode 100644
index 0000000..d3b5710
--- /dev/null
+++ b/control/json/actions/pcie_card_floors.hpp
@@ -0,0 +1,170 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "../zone.hpp"
+#include "action.hpp"
+#include "group.hpp"
+#include "utils/pcie_card_metadata.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+class Manager;
+
+/**
+ * @class PCIeCardFloors - Action to set the PCIe card floor index parameter
+ *                          based on the PCIe cards plugged in the system.
+ *
+ *  - Loads PCIe card metadata files using the PCIeCardMetadata class.
+ *  - Watches for PCIe slots to be powered on (or off).
+ *  - Reads four properties off of the PCIeDevice interface on the powered
+ *    on cards.
+ *  - Looks up the floor index for the card by calling PCIeCardMetadata::lookup
+ *    and passing in the PCIeDevice properties.
+ *  - Sets the pcie_floor_index parameter with the highest floor index found.
+ *  - If no PCIe cards are found, it removes the parameter.
+ *  - If a card isn't recognized, it's ignored since it isn't considered a
+ *    hot card.
+ *  - If a powered on card has its own temperature sensor, then it doesn't
+ *    have a floor index.
+ *  - Since the slot powered on indications are all sent at once, it has a
+ *    small delay that gets started in each run() call that must expire
+ *    before the body of the action is run, so it only runs once.
+ *
+ *    The JSON configuration has two entries:
+ *    {
+ *       "settle_time": <time in s>
+ *       "use_config_specific_files": <true/false>
+ *    }
+ *
+ *    settle_time:
+ *      - Specifies how long to wait after run() is called before actually
+ *        running the action as a flurry of propertiesChanged signals
+ *        that trigger the action tend to come at once.
+ *      - Optional.  If not specified it defaults to zero.
+ *
+ *    use_config_specific_files:
+ *      - If true, will look for 'pcie_cards.json' files in the system
+ *        specific directories.
+ *      - See PCIeCardMetadata for details.
+ */
+class PCIeCardFloors : public ActionBase, public ActionRegister<PCIeCardFloors>
+{
+  public:
+    /* Name of this action */
+    static constexpr auto name = "pcie_card_floors";
+
+    PCIeCardFloors() = delete;
+    PCIeCardFloors(const PCIeCardFloors&) = delete;
+    PCIeCardFloors(PCIeCardFloors&&) = delete;
+    PCIeCardFloors& operator=(const PCIeCardFloors&) = delete;
+    PCIeCardFloors& operator=(PCIeCardFloors&&) = delete;
+    ~PCIeCardFloors() = default;
+
+    /**
+     * @brief Read in the action configuration
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     * @param[in] groups - Groups of dbus objects the action uses
+     */
+    PCIeCardFloors(const json& jsonObj, const std::vector<Group>& groups);
+
+    /**
+     * @brief Run the action.
+     *
+     * Starts/restarts the settle timer and then calls execute()
+     * when it expires.  Done to handle the flood of slot powered
+     * on/off signals.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void run(Zone& zone) override;
+
+  private:
+    /**
+     * @brief Runs the contents of the action when the settle timer expires.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void execute(Zone& zone);
+
+    /**
+     * @brief Constructs the PCIeCardMetadata object to load the PCIe card
+     *        JSON files.
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void loadCardJSON(const json& jsonObj);
+
+    /**
+     * @brief Returns the D-Bus object path of the card plugged into
+     *        the slot represented by the slotPath D-Bus path.
+     *
+     * @param[in] slotPath - The D-Bus path of the PCIe slot object.
+     *
+     * @return const std::string& - The card object path.
+     */
+    const std::string& getCardFromSlot(const std::string& slotPath);
+
+    /**
+     * @brief Returns the floor index (or temp sensor name) for the
+     *        card in the passed in slot.
+     *
+     * @param[in] slotPath - The D-Bus path of the PCIe slot object.
+     *
+     * @return optional<variant<int32_t, string>>
+     *  - The floor index or true for has temp sensor if found,
+     *    std::nullopt else.
+     */
+    std::optional<std::variant<int32_t, bool>>
+        getFloorIndexFromSlot(const std::string& slotPath);
+
+    /**
+     * @brief Gets the hex PCIeDevice property value from the
+     *        manager object cache.
+     *
+     * @param[in] objectPath - The card object path
+     * @param[in] propertyName - The property to read
+     *
+     * @return uint16_t The property value
+     */
+    uint16_t getPCIeDeviceProperty(const std::string& objectPath,
+                                   const std::string& propertyName);
+
+    /* The PCIe card metadata manager */
+    std::unique_ptr<PCIeCardMetadata> _cardMetadata;
+
+    /* Cache map of PCIe slot paths to their plugged card paths */
+    std::unordered_map<std::string, std::string> _cards;
+
+    /* Cache of all objects with a PCIeDevice interface. */
+    std::vector<std::string> _pcieDevices;
+
+    std::chrono::seconds _settleTime{0};
+
+    /* Timer to wait for slot plugs to settle down before running action */
+    std::unique_ptr<Timer> _settleTimer;
+
+    /* Last status printed so only new messages get recorded */
+    std::string _lastStatus;
+};
+
+} // namespace phosphor::fan::control::json