Validate user power cap changes fall within limits

Add validation of the user power cap when changed via Redfish or during
boot.  Validation also needs to ensure the correct power types are being
compared (input/AC vs output/DC).

Verified on hardware with limits set via GUI and Redfish.

Change-Id: Ic0d8ce2df6fdb803bbede7e42f10f34c142c6b07
Signed-off-by: Chris Cain <cjcain@us.ibm.com>
diff --git a/powercap.cpp b/powercap.cpp
index 9b102ab..20a8f47 100644
--- a/powercap.cpp
+++ b/powercap.cpp
@@ -35,16 +35,18 @@
         getPcapFilename(std::regex{"power\\d+_cap_min_soft$"});
     fs::path maxName = getPcapFilename(std::regex{"power\\d+_cap_max$"});
 
-    // Read the power cap bounds from sysfs files
-    uint64_t cap;
-    uint32_t capSoftMin = 0, capHardMin = 0, capMax = INT_MAX;
+    // Read the current cap bounds from dbus
+    uint32_t capSoftMin, capHardMin, capMax;
+    readDbusPcapLimits(capSoftMin, capHardMin, capMax);
 
+    // Read the power cap bounds from sysfs files (from OCC)
+    uint64_t cap;
     std::ifstream softMinFile(softMinName, std::ios::in);
     if (softMinFile)
     {
         softMinFile >> cap;
         softMinFile.close();
-        // Convert to Input Power in Watts (round up)
+        // Convert to input/AC Power in Watts (round up)
         capSoftMin = ((cap / (PS_DERATING_FACTOR / 100.0) / 1000000) + 0.9);
     }
     else
@@ -61,7 +63,7 @@
     {
         minFile >> cap;
         minFile.close();
-        // Convert to Input Power in Watts (round up)
+        // Convert to input/AC Power in Watts (round up)
         capHardMin = ((cap / (PS_DERATING_FACTOR / 100.0) / 1000000) + 0.9);
     }
     else
@@ -78,7 +80,7 @@
     {
         maxFile >> cap;
         maxFile.close();
-        // Convert to Input Power in Watts (truncate remainder)
+        // Convert to input/AC Power in Watts (truncate remainder)
         capMax = cap / (PS_DERATING_FACTOR / 100.0) / 1000000;
     }
     else
@@ -90,24 +92,69 @@
                 .c_str());
     }
 
-    // Save the bounds to dbus
-    updateDbusPcap(capSoftMin, capHardMin, capMax);
+    // Save the power cap bounds to dbus
+    updateDbusPcapLimits(capSoftMin, capHardMin, capMax);
 
-    // Validate dbus and hwmon user caps match
+    // Validate user power cap (if enabled) is within the bounds
     const uint32_t dbusUserCap = getPcap();
-    const uint32_t hwmonUserCap = readUserCapHwmon();
-    if ((hwmonUserCap != 0) && (dbusUserCap != hwmonUserCap))
+    const bool pcapEnabled = getPcapEnabled();
+    if (pcapEnabled && (dbusUserCap != 0))
     {
-        // User power cap is enabled, but does not match dbus
-        log<level::ERR>(
-            fmt::format(
-                "updatePcapBounds: user powercap mismatch (hwmon:{}W, bdus:{}W) - using dbus",
-                hwmonUserCap, dbusUserCap)
-                .c_str());
-        writeOcc(dbusUserCap);
+        const uint32_t hwmonUserCap = readUserCapHwmon();
+        if ((dbusUserCap >= capSoftMin) && (dbusUserCap <= capMax))
+        {
+            // Validate dbus and hwmon user caps match
+            if ((hwmonUserCap != 0) && (dbusUserCap != hwmonUserCap))
+            {
+                // User power cap is enabled, but does not match dbus
+                log<level::ERR>(
+                    fmt::format(
+                        "updatePcapBounds: user powercap mismatch (hwmon:{}W, bdus:{}W) - using dbus",
+                        hwmonUserCap, dbusUserCap)
+                        .c_str());
+                auto occInput = getOccInput(dbusUserCap, pcapEnabled);
+                writeOcc(occInput);
+            }
+        }
+        else
+        {
+            // User power cap is outside of current bounds
+            uint32_t newCap = capMax;
+            if (dbusUserCap < capSoftMin)
+            {
+                newCap = capSoftMin;
+            }
+            log<level::ERR>(
+                fmt::format(
+                    "updatePcapBounds: user powercap {}W is outside bounds "
+                    "(soft min:{}, min:{}, max:{})",
+                    dbusUserCap, capSoftMin, capHardMin, capMax)
+                    .c_str());
+            try
+            {
+                log<level::INFO>(
+                    fmt::format(
+                        "updatePcapBounds: Updating user powercap from {} to {}W",
+                        hwmonUserCap, newCap)
+                        .c_str());
+                utils::setProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_PROP,
+                                   newCap);
+                auto occInput = getOccInput(newCap, pcapEnabled);
+                writeOcc(occInput);
+            }
+            catch (const sdbusplus::exception::exception& e)
+            {
+                log<level::ERR>(
+                    fmt::format(
+                        "updatePcapBounds: Failed to update user powercap due to ",
+                        e.what())
+                        .c_str());
+            }
+        }
     }
 }
 
+// Get value of power cap to send to the OCC (output/DC power)
 uint32_t PowerCap::getOccInput(uint32_t pcap, bool pcapEnabled)
 {
     if (!pcapEnabled)
@@ -117,7 +164,7 @@
     }
 
     // If pcap is not disabled then just return the pcap with the derating
-    // factor applied.
+    // factor applied (output/DC power).
     return ((static_cast<uint64_t>(pcap) * PS_DERATING_FACTOR) / 100);
 }
 
@@ -190,7 +237,7 @@
     return fs::path{};
 }
 
-// Write the user power cap to sysfs.
+// Write the user power cap to sysfs (output/DC power)
 // This will trigger the driver to send the cap to the OCC
 void PowerCap::writeOcc(uint32_t pcapValue)
 {
@@ -235,7 +282,7 @@
     return;
 }
 
-// Read the current user power cap from sysfs.
+// Read the current user power cap from sysfs as input/AC power
 uint32_t PowerCap::readUserCapHwmon()
 {
     uint32_t userCap = 0;
@@ -259,8 +306,8 @@
         uint64_t cap;
         file >> cap;
         file.close();
-        // Convert to Watts
-        userCap = cap / 1000000;
+        // Convert to input/AC Power in Watts
+        userCap = (cap / (PS_DERATING_FACTOR / 100.0) / 1000000);
     }
     else
     {
@@ -314,6 +361,30 @@
         }
     }
 
+    // Validate the cap is within supported range
+    uint32_t capSoftMin, capHardMin, capMax;
+    readDbusPcapLimits(capSoftMin, capHardMin, capMax);
+    if (((pcap > 0) && (pcap < capSoftMin)) || ((pcap == 0) && (pcapEnabled)))
+    {
+        log<level::ERR>(
+            fmt::format(
+                "pcapChanged: Power cap of {}W is lower than allowed (soft min:{}, min:{}) - using soft min",
+                pcap, capSoftMin, capHardMin)
+                .c_str());
+        pcap = capSoftMin;
+        utils::setProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_PROP, pcap);
+    }
+    else if (pcap > capMax)
+    {
+        log<level::ERR>(
+            fmt::format(
+                "pcapChanged: Power cap of {}W is higher than allowed (max:{}) - using max",
+                pcap, capSoftMin, capHardMin)
+                .c_str());
+        pcap = capMax;
+        utils::setProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_PROP, pcap);
+    }
+
     if (changeFound)
     {
         log<level::INFO>(
@@ -323,9 +394,7 @@
                 .c_str());
 
         // Determine desired action to write to occ
-
         auto occInput = getOccInput(pcap, pcapEnabled);
-
         // Write action to occ
         writeOcc(occInput);
     }
@@ -334,7 +403,8 @@
 }
 
 // Update the Power Cap bounds on DBus
-bool PowerCap::updateDbusPcap(uint32_t softMin, uint32_t hardMin, uint32_t max)
+bool PowerCap::updateDbusPcapLimits(uint32_t softMin, uint32_t hardMin,
+                                    uint32_t max)
 {
     bool complete = true;
 
@@ -347,7 +417,7 @@
     {
         log<level::ERR>(
             fmt::format(
-                "updateDbusPcap: Failed to set SOFT PCAP to {}W due to {}",
+                "updateDbusPcapLimits: Failed to set SOFT PCAP to {}W due to {}",
                 softMin, e.what())
                 .c_str());
         complete = false;
@@ -362,7 +432,7 @@
     {
         log<level::ERR>(
             fmt::format(
-                "updateDbusPcap: Failed to set HARD PCAP to {}W due to {}",
+                "updateDbusPcapLimits: Failed to set HARD PCAP to {}W due to {}",
                 hardMin, e.what())
                 .c_str());
         complete = false;
@@ -376,14 +446,72 @@
     {
         log<level::ERR>(
             fmt::format(
-                "updateDbusPcap: Failed to set MAX PCAP to {}W due to {}", max,
-                e.what())
+                "updateDbusPcapLimits: Failed to set MAX PCAP to {}W due to {}",
+                max, e.what())
                 .c_str());
         complete = false;
     }
 
     return complete;
 }
+
+// Read the Power Cap bounds from DBus
+bool PowerCap::readDbusPcapLimits(uint32_t& softMin, uint32_t& hardMin,
+                                  uint32_t& max)
+{
+    bool complete = true;
+    utils::PropertyValue prop{};
+
+    try
+    {
+        prop =
+            utils::getProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_SOFT_MIN);
+        softMin = std::get<uint32_t>(prop);
+    }
+    catch (const sdbusplus::exception::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format("readDbusPcapLimits: Failed to get SOFT PCAP due to {}",
+                        e.what())
+                .c_str());
+        softMin = 0;
+        complete = false;
+    }
+
+    try
+    {
+        prop =
+            utils::getProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_HARD_MIN);
+        hardMin = std::get<uint32_t>(prop);
+    }
+    catch (const sdbusplus::exception::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format("readDbusPcapLimits: Failed to get HARD PCAP due to {}",
+                        e.what())
+                .c_str());
+        hardMin = 0;
+        complete = false;
+    }
+
+    try
+    {
+        prop = utils::getProperty(PCAP_PATH, PCAP_INTERFACE, POWER_CAP_MAX);
+        max = std::get<uint32_t>(prop);
+    }
+    catch (const sdbusplus::exception::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format("readDbusPcapLimits: Failed to get MAX PCAP due to {}",
+                        e.what())
+                .c_str());
+        max = INT_MAX;
+        complete = false;
+    }
+
+    return complete;
+}
+
 } // namespace powercap
 
 } // namespace occ