Add support for Entity Association Record

Adding support for Entity Association Record (SDR type - 0x08h)
This patch includes:
1) Entity Association Record yaml file example
2) Entity Assocation Record related script and mako file changes
3) Adding Entity Association Record in get_sdr IPMI command response

From the host, tested that entity association records can be fetched

Change-Id: I9cf598e5d27d2e8c6751bbaae2176e7c976974b1
Tested: Yes
Signed-off-by: Jaghathiswari Rankappagounder Natarajan <jaghu@google.com>
diff --git a/Makefile.am b/Makefile.am
index 9bf6d4e..20af6ed 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -20,7 +20,8 @@
 libipmi20_BUILT_LIST = \
 	sensor-gen.cpp \
 	inventory-sensor-gen.cpp \
-	fru-read-gen.cpp
+	fru-read-gen.cpp \
+	entity-gen.cpp
 
 BUILT_SOURCES = \
 	ipmiwhitelist.cpp \
@@ -70,6 +71,9 @@
 fru-read-gen.cpp:
 	$(AM_V_GEN)@FRUGEN@ -o $(top_builddir) generate-cpp
 
+entity-gen.cpp:
+	$(AM_V_GEN)@ENTITYGEN@ -o $(top_builddir) generate-cpp
+
 providers_LTLIBRARIES += libipmi20.la
 libipmi20_la_SOURCES = \
 	app/channel.cpp \
diff --git a/configure.ac b/configure.ac
index 8ad85f2..471a7be 100644
--- a/configure.ac
+++ b/configure.ac
@@ -97,6 +97,10 @@
 FRUGEN="$PYTHON $srcdir/scripts/fru_gen.py -i $FRU_YAML_GEN"
 AC_SUBST(FRUGEN)
 
+AS_IF([test "x$ENTITY_YAML_GEN" == "x"], [ENTITY_YAML_GEN="entity-example.yaml"])
+ENTITYGEN="$PYTHON $srcdir/scripts/entity_gen.py -i $ENTITY_YAML_GEN"
+AC_SUBST(ENTITYGEN)
+
 AC_DEFINE(CALLOUT_FWD_ASSOCIATION, "callout", [The name of the callout's forward association.])
 AC_DEFINE(BOARD_SENSOR, "/xyz/openbmc_project/inventory/system/chassis/motherboard", [The inventory path to the motherboard fault sensor.])
 AC_DEFINE(SYSTEM_SENSOR, "/xyz/openbmc_project/inventory/system", [The inventory path to the system event sensor.])
diff --git a/scripts/entity-example.yaml b/scripts/entity-example.yaml
new file mode 100755
index 0000000..2db14cc
--- /dev/null
+++ b/scripts/entity-example.yaml
@@ -0,0 +1,127 @@
+# This record has:
+# Container Entity Id and Container Entity Instance = (0x13, 0x81)
+# Contained Entity Id and Contained Entity Instance = (0x0A, 0x1),
+# (0x0A, 0x3), (0x0A, 0x5), (0x0A, 0x7)
+# Entity Record id is the key
+0x01:
+  # Container entity contains other entities
+  # Entity Id and entity Instance for the container entity
+  containerEntityId: 0x13
+  containerEntityInstance: 0x81
+  # A record can have contained entities as a four entry list or as upto
+  # two ranges of entity instances; this record has contained entities
+  # as a four entry list
+  isList: "true"
+  # Records can be linked if necessary to extend the number of contained
+  # entities within a container entity; this record is not linked
+  isLinked: "false"
+  entityId1: 0x0A
+  entityInstance1: 0x1
+  entityId2: 0x0A
+  entityInstance2: 0x3
+  entityId3: 0x0A
+  entityInstance3: 0x5
+  entityId4: 0x0A
+  entityInstance4: 0x7
+
+# The below two records have:
+# Container Entity Id and Container Entity Instance = (0x18, 0x2)
+# Contained Entity Id and Contained Entity Instance = (0x1D, 0x1),
+# (0x1D, 0x4), (0x1D, 0x6), (0x2B, 0x1), (0x2B, 0x3), (0x0F, 0x1),
+# (0x0F, 0x3), (0x10, 0x5)
+0x02:
+  containerEntityId: 0x18
+  containerEntityInstance: 0x2
+  # This record  has contained entities as a four entry list
+  isList: "true"
+  # This record is linked with the below record; this record and the
+  # below record have the same container entity Id and container entity
+  # instance;
+  isLinked: "true"
+  entityId1: 0x1D
+  entityInstance1: 0x1
+  entityId2: 0x1D
+  entityInstance2: 0x4
+  entityId3: 0x1D
+  entityInstance3: 0x6
+  entityId4: 0x2B
+  entityInstance4: 0x1
+
+0x03:
+  containerEntityId: 0x18
+  containerEntityInstance: 0x2
+  # This record  has contained entities as a four entry list
+  isList: "true"
+  # This record is linked with the above record; this record and the
+  # above record have the same container entity Id and container entity
+  # instance
+  isLinked: "true"
+  entityId1: 0x2B
+  entityInstance1: 0x3
+  entityId2: 0x0F
+  entityInstance2: 0x1
+  entityId3: 0x0F
+  entityInstance3: 0x3
+  entityId4: 0x10
+  entityInstance4: 0x5
+
+# This record has:
+# Container Entity Id and Container Entity Instance = (0x1E, 0x1)
+# Contained Entity Id and Contained Entity Instance = (0x20, 0x1),
+# (0x20, 0x2), (0x20, 0x3), (0x20, 0x7), (0x20, 0x8), (0x20, 0x9)
+0x04:
+  containerEntityId: 0x1E
+  containerEntityInstance: 0x1
+  # This record has contained entities as two ranges of entity instances
+  isList: "false"
+  # This record is not linked
+  isLinked: "false"
+  entityId1: 0x20
+  entityInstance1: 0x1
+  entityId2: 0x20
+  entityInstance2: 0x3
+  entityId3: 0x20
+  entityInstance3: 0x7
+  entityId4: 0x20
+  entityInstance4: 0x9
+
+# The below two records have:
+# Container Entity Id and Container Entity Instance = (0x1E, 0x3)
+# Contained Entity Id and Contained Entity Instance = (0x20, 0x1),
+# (0x20, 0x2), (0x20, 0x3), (0x20, 0x6), (0x20, 0x7), (0x20, 0x8),
+# (0x20, 0xA), (0x20, 0xB), (0x20, 0xD), (0x20, 0xE), (0x20, 0xF)
+0x05:
+  containerEntityId: 0x1E
+  containerEntityInstance: 0x03
+  # This record has contained entities as two ranges of entity instances
+  isList: "false"
+  # This record is linked with the below record; this record and the
+  # below record have the same container entity Id and container entity
+  # instance;
+  isLinked: "true"
+  entityId1: 0x20
+  entityInstance1: 0x1
+  entityId2: 0x20
+  entityInstance2: 0x3
+  entityId3: 0x20
+  entityInstance3: 0x6
+  entityId4: 0x20
+  entityInstance4: 0x8
+
+0x06:
+  containerEntityId: 0x1E
+  containerEntityInstance: 0x03
+  # This record has contained entities as two ranges of entity instances
+  isList: "false"
+  # This record is linked with the above record; this record and the
+  # above record have the same container entity Id and container entity
+  # instance;
+  isLinked: "true"
+  entityId1: 0x20
+  entityInstance1: 0xA
+  entityId2: 0x20
+  entityInstance2: 0xB
+  entityId3: 0x20
+  entityInstance3: 0xD
+  entityId4: 0x20
+  entityInstance4: 0xF
diff --git a/scripts/entity_gen.py b/scripts/entity_gen.py
new file mode 100755
index 0000000..057821b
--- /dev/null
+++ b/scripts/entity_gen.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import yaml
+import argparse
+from mako.template import Template
+
+
+def generate_cpp(entity_yaml, output_dir):
+    with open(os.path.join(script_dir, entity_yaml), 'r') as f:
+        ifile = yaml.safe_load(f)
+        if not isinstance(ifile, dict):
+            ifile = {}
+
+        # Render the mako template
+
+        t = Template(filename=os.path.join(
+                     script_dir,
+                     "writeentity.mako.cpp"))
+
+        output_cpp = os.path.join(output_dir, "entity-gen.cpp")
+        with open(output_cpp, 'w') as fd:
+            fd.write(t.render(entityDict=ifile))
+
+
+def main():
+
+    valid_commands = {
+        'generate-cpp': generate_cpp
+    }
+    parser = argparse.ArgumentParser(
+        description="IPMI Entity record parser and code generator")
+
+    parser.add_argument(
+        '-i', '--entity_yaml', dest='entity_yaml',
+        default='example.yaml', help='input entity yaml file to parse')
+
+    parser.add_argument(
+        "-o", "--output-dir", dest="outputdir",
+        default=".",
+        help="output directory")
+
+    parser.add_argument(
+        'command', metavar='COMMAND', type=str,
+        choices=valid_commands.keys(),
+        help='Command to run.')
+
+    args = parser.parse_args()
+
+    if (not (os.path.isfile(os.path.join(script_dir, args.entity_yaml)))):
+        sys.exit("Can not find input yaml file " + args.entity_yaml)
+
+    function = valid_commands[args.command]
+    function(args.entity_yaml, args.outputdir)
+
+
+if __name__ == '__main__':
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    main()
diff --git a/scripts/writeentity.mako.cpp b/scripts/writeentity.mako.cpp
new file mode 100644
index 0000000..9de11a0
--- /dev/null
+++ b/scripts/writeentity.mako.cpp
@@ -0,0 +1,34 @@
+## This file is a template.  The comment below is emitted
+## into the rendered file; feel free to edit this file.
+// !!! WARNING: This is a GENERATED Code..Please do NOT Edit !!!
+
+#include "types.hpp"
+using namespace ipmi::sensor;
+
+extern const EntityInfoMap entities = {
+% for key in entityDict.iterkeys():
+{${key},{
+<%
+       entity = entityDict[key]
+       containerEntityId = entity["containerEntityId"]
+       containerEntityInstance = entity["containerEntityInstance"]
+       isList = entity["isList"]
+       isLinked = entity["isLinked"]
+       entityId1 = entity["entityId1"]
+       entityInstance1 = entity["entityInstance1"]
+       entityId2 = entity["entityId2"]
+       entityInstance2 = entity["entityInstance2"]
+       entityId3 = entity["entityId3"]
+       entityInstance3 = entity["entityInstance3"]
+       entityId4 = entity["entityId4"]
+       entityInstance4 = entity["entityInstance4"]
+%>
+        ${containerEntityId},${containerEntityInstance},${isList},${isLinked},{
+          std::make_pair(${entityId1}, ${entityInstance1}),
+          std::make_pair(${entityId2}, ${entityInstance2}),
+          std::make_pair(${entityId3}, ${entityInstance3}),
+          std::make_pair(${entityId4}, ${entityInstance4}) }
+
+}},
+% endfor
+};
diff --git a/sensorhandler.cpp b/sensorhandler.cpp
index 40c0d3e..9d0364d 100644
--- a/sensorhandler.cpp
+++ b/sensorhandler.cpp
@@ -27,6 +27,7 @@
 extern sd_bus* bus;
 extern const ipmi::sensor::IdInfoMap sensors;
 extern const FruMap frus;
+extern const ipmi::sensor::EntityInfoMap entities;
 
 using namespace phosphor::logging;
 using InternalFailure =
@@ -575,7 +576,7 @@
         get_sdr_info::request::get_count(request) == false)
     {
         // Get Sensor Count
-        resp->count = sensors.size() + frus.size();
+        resp->count = sensors.size() + frus.size() + entities.size();
     }
     else
     {
@@ -748,13 +749,87 @@
 
     if (++fru == frus.end())
     {
+        // we have reached till end of fru, so assign the next record id to
+        // 512(Max fru ID = 511) + Entity Record ID(may start with 0).
+        auto next_record_id =
+            (entities.size()) ? entities.begin()->first + ENTITY_RECORD_ID_START
+                              : END_OF_RECORD;
+        get_sdr::response::set_next_record_id(next_record_id, resp);
+    }
+    else
+    {
+        get_sdr::response::set_next_record_id(
+            (FRU_RECORD_ID_START + fru->first), resp);
+    }
+
+    // Check for invalid offset size
+    if (req->offset > sizeof(record))
+    {
+        return IPMI_CC_PARM_OUT_OF_RANGE;
+    }
+
+    dataLength = std::min(static_cast<size_t>(req->bytes_to_read),
+                          sizeof(record) - req->offset);
+
+    std::memcpy(resp->record_data,
+                reinterpret_cast<uint8_t*>(&record) + req->offset, dataLength);
+
+    *data_len = dataLength;
+    *data_len += 2; // additional 2 bytes for next record ID
+
+    return IPMI_CC_OK;
+}
+
+ipmi_ret_t ipmi_entity_get_sdr(ipmi_request_t request, ipmi_response_t response,
+                               ipmi_data_len_t data_len)
+{
+    auto req = reinterpret_cast<get_sdr::GetSdrReq*>(request);
+    auto resp = reinterpret_cast<get_sdr::GetSdrResp*>(response);
+    get_sdr::SensorDataEntityRecord record{};
+    auto dataLength = 0;
+
+    auto entity = entities.begin();
+    uint8_t entityRecordID;
+    auto recordID = get_sdr::request::get_record_id(req);
+
+    entityRecordID = recordID - ENTITY_RECORD_ID_START;
+    entity = entities.find(entityRecordID);
+    if (entity == entities.end())
+    {
+        return IPMI_CC_SENSOR_INVALID;
+    }
+
+    /* Header */
+    get_sdr::header::set_record_id(recordID, &(record.header));
+    record.header.sdr_version = SDR_VERSION; // Based on IPMI Spec v2.0 rev 1.1
+    record.header.record_type = get_sdr::SENSOR_DATA_ENTITY_RECORD;
+    record.header.record_length = sizeof(record.key) + sizeof(record.body);
+
+    /* Key */
+    record.key.containerEntityId = entity->second.containerEntityId;
+    record.key.containerEntityInstance = entity->second.containerEntityInstance;
+    get_sdr::key::set_flags(entity->second.isList, entity->second.isLinked,
+                            &(record.key));
+    record.key.entityId1 = entity->second.containedEntities[0].first;
+    record.key.entityInstance1 = entity->second.containedEntities[0].second;
+
+    /* Body */
+    record.body.entityId2 = entity->second.containedEntities[1].first;
+    record.body.entityInstance2 = entity->second.containedEntities[1].second;
+    record.body.entityId3 = entity->second.containedEntities[2].first;
+    record.body.entityInstance3 = entity->second.containedEntities[2].second;
+    record.body.entityId4 = entity->second.containedEntities[3].first;
+    record.body.entityInstance4 = entity->second.containedEntities[3].second;
+
+    if (++entity == entities.end())
+    {
         get_sdr::response::set_next_record_id(END_OF_RECORD,
                                               resp); // last record
     }
     else
     {
         get_sdr::response::set_next_record_id(
-            (FRU_RECORD_ID_START + fru->first), resp);
+            (ENTITY_RECORD_ID_START + entity->first), resp);
     }
 
     // Check for invalid offset size
@@ -793,10 +868,17 @@
         // At the beginning of a scan, the host side will send us id=0.
         if (recordID != 0)
         {
-            // recordID greater then 255,it means it is a FRU record.
-            // Currently we are supporting two record types either FULL record
-            // or FRU record.
-            if (recordID >= FRU_RECORD_ID_START)
+            // recordID 0 to 255 means it is a FULL record.
+            // recordID 256 to 511 means it is a FRU record.
+            // recordID greater then 511 means it is a Entity Association
+            // record. Currently we are supporting three record types: FULL
+            // record, FRU record and Enttiy Association record.
+            if (recordID >= ENTITY_RECORD_ID_START)
+            {
+                return ipmi_entity_get_sdr(request, response, data_len);
+            }
+            else if (recordID >= FRU_RECORD_ID_START &&
+                     recordID < ENTITY_RECORD_ID_START)
             {
                 return ipmi_fru_get_sdr(request, response, data_len);
             }
diff --git a/sensorhandler.hpp b/sensorhandler.hpp
index 0338597..8fcf2b1 100644
--- a/sensorhandler.hpp
+++ b/sensorhandler.hpp
@@ -86,6 +86,7 @@
                                 ipmi_context_t context);
 
 static const uint16_t FRU_RECORD_ID_START = 256;
+static const uint16_t ENTITY_RECORD_ID_START = 512;
 static const uint8_t SDR_VERSION = 0x51;
 static const uint16_t END_OF_RECORD = 0xFFFF;
 static const uint8_t LENGTH_MASK = 0x1F;
@@ -210,6 +211,7 @@
 {
     SENSOR_DATA_FULL_RECORD = 0x1,
     SENSOR_DATA_FRU_RECORD = 0x11,
+    SENSOR_DATA_ENTITY_RECORD = 0x8,
 };
 
 // Record key
@@ -232,9 +234,25 @@
     uint8_t channelNumber;
 } __attribute__((packed));
 
+/** @struct SensorDataEntityRecordKey
+ *
+ *  Entity Association Record(key) - SDR Type 8
+ */
+struct SensorDataEntityRecordKey
+{
+    uint8_t containerEntityId;
+    uint8_t containerEntityInstance;
+    uint8_t flags;
+    uint8_t entityId1;
+    uint8_t entityInstance1;
+} __attribute__((packed));
+
 namespace key
 {
 
+static constexpr uint8_t listOrRangeBit = 7;
+static constexpr uint8_t linkedBit = 6;
+
 inline void set_owner_id_ipmb(SensorDataRecordKey* key)
 {
     key->owner_id &= ~0x01;
@@ -268,6 +286,17 @@
     key->owner_lun |= ((channel & 0xf) << 4);
 };
 
+inline void set_flags(bool isList, bool isLinked,
+                      SensorDataEntityRecordKey* key)
+{
+    key->flags = 0x00;
+    if (!isList)
+        key->flags |= 1 << listOrRangeBit;
+
+    if (isLinked)
+        key->flags |= 1 << linkedBit;
+};
+
 } // namespace key
 
 /** @struct GetSensorThresholdsResponse
@@ -347,6 +376,20 @@
     char deviceID[FRU_RECORD_DEVICE_ID_MAX_LENGTH];
 } __attribute__((packed));
 
+/** @struct SensorDataEntityRecordBody
+ *
+ *  Entity Association Record(body) - SDR Type 8
+ */
+struct SensorDataEntityRecordBody
+{
+    uint8_t entityId2;
+    uint8_t entityInstance2;
+    uint8_t entityId3;
+    uint8_t entityInstance3;
+    uint8_t entityId4;
+    uint8_t entityInstance4;
+} __attribute__((packed));
+
 namespace body
 {
 
@@ -594,6 +637,17 @@
     SensorDataFruRecordBody body;
 } __attribute__((packed));
 
+/** @struct SensorDataEntityRecord
+ *
+ *  Entity Association Record - SDR Type 8
+ */
+struct SensorDataEntityRecord
+{
+    SensorDataRecordHeader header;
+    SensorDataEntityRecordKey key;
+    SensorDataEntityRecordBody body;
+} __attribute__((packed));
+
 } // namespace get_sdr
 
 namespace ipmi
diff --git a/types.hpp b/types.hpp
index 70f397c..57c5873 100644
--- a/types.hpp
+++ b/types.hpp
@@ -196,6 +196,21 @@
     CRITICAL_HIGH_MASK = 0x10,
 };
 
+static constexpr uint8_t maxContainedEntities = 4;
+using ContainedEntitiesArray =
+    std::array<std::pair<uint8_t, uint8_t>, maxContainedEntities>;
+
+struct EntityInfo
+{
+    uint8_t containerEntityId;
+    uint8_t containerEntityInstance;
+    bool isList;
+    bool isLinked;
+    ContainedEntitiesArray containedEntities;
+};
+
+using EntityInfoMap = std::map<Id, EntityInfo>;
+
 } // namespace sensor
 
 namespace network