em: topology: Fix late add  support + LocationCode

Patch in the fixes that already in upstream for creating topology on
devices that's added after the initial scan.

Added the LocationCode support to match the existing feature set with
the old i2c topology implementation.

Google-Bug-Id: 261095331
Google-Bug-Id: 302709931
Google-Bug-Id: 302708516
Change-Id: I914f5b4fc646e1b7bfed58fb44d34499fde8f6b4
Signed-off-by: Willy Tu <wltu@google.com>
(cherry picked from commit 5063f22c19b04baeed94167fab17b573a4f37b0c)
diff --git a/recipes-phosphor/configuration/entity-manager/0001-phys-topology-Add-late-add-remove-support.patch b/recipes-phosphor/configuration/entity-manager/0001-phys-topology-Add-late-add-remove-support.patch
new file mode 100644
index 0000000..66b736e
--- /dev/null
+++ b/recipes-phosphor/configuration/entity-manager/0001-phys-topology-Add-late-add-remove-support.patch
@@ -0,0 +1,612 @@
+From c458dd294a9b7d65bf4f579ff34b10c8056eecec Mon Sep 17 00:00:00 2001
+From: Matt Spinler <spinler@us.ibm.com>
+Date: Mon, 14 Aug 2023 16:36:20 -0500
+Subject: [PATCH] phys-topology: Add late add/remove support
+
+The current physical topology code doesn't yet support adding entities
+late (in a different propertiesChangedCallback call) or removing an
+entity because:
+- The Topology class is just scoped to postToDbus(), so when that is
+  called again later with new cards it has no concept of existing parent
+  cards so it will miss creating associations.
+
+- There is nothing to tell the class when an entity is removed, so it
+  never attempts to remove the association for that entity.
+
+- When the containing/contained_by association is created it doesn't use
+  the createInterface() function, so if that entity is removed later
+  that association interface will be left on D-Bus.
+
+To add support for entity adds and removes, this commit will:
+- Make the Topology class have a global scoped lifetime so it can
+  remember entity relationships.
+
+- Now that Topology will outlive postToDbus() calls, pass the
+  getAssocs() method the list of boards being processed in the current
+  postToDbus() incantation so it will only return the new associations.
+
+- Use the createInterface() method when creating the association. This
+  stores the interface in a map with the entity name so that when the
+  entity is removed the interface will be removed along with all the
+  other interfaces.
+
+- When an entity is removed, only the board name is known.  So pass the
+  board name into addBoard() so the Topology class knows it, and add a
+  Topology::remove() method and call it so it can remove the removed
+  path from all of the connector maps.
+
+Tested:
+- All of the containing/contained_by associations still show up on good
+  path.
+
+- Added new unit tests to cover the new functionality.
+
+- When a downstream entity is added after EM does its initial D-Bus
+  publish, the containing/contained_by association is now created.
+
+- On an entity remove, there are no left over interfaces for the removed
+  entity on D-Bus.
+
+- When the removed entity is added back, the association is put back in
+  place.
+
+Patch Tracking Bug: b/302708516
+Upstream info / review:
+https://gerrit.openbmc.org/c/openbmc/entity-manager/+/65938
+Upstream-Status: Accepted
+Justification: Needs the rebase to pick up this change in gBMC.
+
+** [reason] can be: native / licensing / configuration / enable feature / disable feature / bugfix(b/<bugID>) / embedded specific / other (give details in comments)
+
+Signed-off-by: Matt Spinler <spinler@us.ibm.com>
+Change-Id: Ie5daaca92c6d2e6e7abc408f3e67e948977581ef
+---
+ src/entity_manager.cpp |  40 ++++++----
+ src/topology.cpp       |  69 ++++++++++++++++-
+ src/topology.hpp       |   7 +-
+ test/test_topology.cpp | 168 +++++++++++++++++++++++++++++++++--------
+ 4 files changed, 234 insertions(+), 50 deletions(-)
+
+diff --git a/src/entity_manager.cpp b/src/entity_manager.cpp
+index cc840ac..621d97a 100644
+--- a/src/entity_manager.cpp
++++ b/src/entity_manager.cpp
+@@ -76,6 +76,7 @@ boost::container::flat_map<
+ // todo: pass this through nicer
+ std::shared_ptr<sdbusplus::asio::connection> systemBus;
+ nlohmann::json lastJson;
++Topology topology;
+ 
+ boost::asio::io_context io;
+ 
+@@ -197,7 +198,7 @@ void addArrayToDbus(const std::string& name, const nlohmann::json& array,
+                 return -1;
+             }
+             return 1;
+-            });
++        });
+     }
+ }
+ 
+@@ -230,7 +231,7 @@ void addProperty(const std::string& name, const PropertyType& value,
+             return -1;
+         }
+         return 1;
+-        });
++    });
+ }
+ 
+ void createDeleteObjectMethod(
+@@ -465,7 +466,7 @@ void createAddObjectMethod(const std::string& jsonPointerPath,
+             std::visit(
+                 [&newJson](auto&& val) {
+                 newJson = std::forward<decltype(val)>(val);
+-                },
++            },
+                 item.second);
+         }
+ 
+@@ -557,7 +558,7 @@ void createAddObjectMethod(const std::string& jsonPointerPath,
+             jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
+             interface, newData, objServer,
+             sdbusplus::asio::PropertyPermission::readWrite);
+-        });
++    });
+     iface->initialize();
+ }
+ 
+@@ -566,7 +567,7 @@ void postToDbus(const nlohmann::json& newConfiguration,
+                 sdbusplus::asio::object_server& objServer)
+ 
+ {
+-    Topology topology;
++    std::map<std::string, std::string> newBoards; // path -> name
+ 
+     // iterate through boards
+     for (const auto& [boardId, boardConfig] : newConfiguration.items())
+@@ -809,16 +810,26 @@ void postToDbus(const nlohmann::json& newConfiguration,
+                 }
+             }
+ 
+-            topology.addBoard(boardName, boardType, item);
++            topology.addBoard(boardName, boardType, boardKeyOrig, item);
+         }
++
++        newBoards.emplace(boardName, boardKeyOrig);
+     }
+ 
+-    for (const auto& boardAssoc : topology.getAssocs())
++    for (const auto& [assocPath, assocPropValue] :
++         topology.getAssocs(newBoards))
+     {
+-        auto ifacePtr = objServer.add_interface(
+-            boardAssoc.first, "xyz.openbmc_project.Association.Definitions");
++        auto findBoard = newBoards.find(assocPath);
++        if (findBoard == newBoards.end())
++        {
++            continue;
++        }
+ 
+-        ifacePtr->register_property("Associations", boardAssoc.second);
++        auto ifacePtr = createInterface(
++            objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
++            findBoard->second);
++
++        ifacePtr->register_property("Associations", assocPropValue);
+         ifacePtr->initialize();
+     }
+ }
+@@ -1001,6 +1012,7 @@ static void pruneConfiguration(nlohmann::json& systemConfiguration,
+ 
+     ifaces.clear();
+     systemConfiguration.erase(name);
++    topology.remove(device["Name"].get<std::string>());
+     logDeviceRemoved(device);
+ }
+ 
+@@ -1128,7 +1140,7 @@ void propertiesChangedCallback(nlohmann::json& systemConfiguration,
+                                     count, std::ref(timer),
+                                     std::ref(systemConfiguration),
+                                     newConfiguration, std::ref(objServer)));
+-            });
++        });
+         perfScan->run();
+     });
+ }
+@@ -1278,7 +1290,7 @@ int main()
+         }
+ 
+         propertiesChangedCallback(systemConfiguration, objServer);
+-        });
++    });
+     // We also need a poke from DBus when new interfaces are created or
+     // destroyed.
+     sdbusplus::bus::match_t interfacesAddedMatch(
+@@ -1289,7 +1301,7 @@ int main()
+         {
+             propertiesChangedCallback(systemConfiguration, objServer);
+         }
+-        });
++    });
+     sdbusplus::bus::match_t interfacesRemovedMatch(
+         static_cast<sdbusplus::bus_t&>(*systemBus),
+         sdbusplus::bus::match::rules::interfacesRemoved(),
+@@ -1298,7 +1310,7 @@ int main()
+         {
+             propertiesChangedCallback(systemConfiguration, objServer);
+         }
+-        });
++    });
+ 
+     boost::asio::post(io, [&]() {
+         propertiesChangedCallback(systemConfiguration, objServer);
+diff --git a/src/topology.cpp b/src/topology.cpp
+index 02e7458..f2ee22b 100644
+--- a/src/topology.cpp
++++ b/src/topology.cpp
+@@ -3,6 +3,7 @@
+ #include <iostream>
+ 
+ void Topology::addBoard(const std::string& path, const std::string& boardType,
++                        const std::string& boardName,
+                         const nlohmann::json& exposesItem)
+ {
+     auto findType = exposesItem.find("Type");
+@@ -10,6 +11,9 @@ void Topology::addBoard(const std::string& path, const std::string& boardType,
+     {
+         return;
+     }
++
++    boardNames.try_emplace(boardName, path);
++
+     PortType exposesType = findType->get<std::string>();
+ 
+     if (exposesType == "DownstreamPort")
+@@ -33,7 +37,8 @@ void Topology::addBoard(const std::string& path, const std::string& boardType,
+     }
+ }
+ 
+-std::unordered_map<std::string, std::vector<Association>> Topology::getAssocs()
++std::unordered_map<std::string, std::vector<Association>>
++    Topology::getAssocs(const std::map<Path, BoardName>& boards)
+ {
+     std::unordered_map<std::string, std::vector<Association>> result;
+ 
+@@ -55,8 +60,12 @@ std::unordered_map<std::string, std::vector<Association>> Topology::getAssocs()
+             {
+                 for (const Path& downstream : downstreamMatch->second)
+                 {
+-                    result[downstream].emplace_back("contained_by",
+-                                                    "containing", upstream);
++                    // The downstream path must be one we care about.
++                    if (boards.find(downstream) != boards.end())
++                    {
++                        result[downstream].emplace_back("contained_by",
++                                                        "containing", upstream);
++                    }
+                 }
+             }
+         }
+@@ -64,3 +73,57 @@ std::unordered_map<std::string, std::vector<Association>> Topology::getAssocs()
+ 
+     return result;
+ }
++
++void Topology::remove(const std::string& boardName)
++{
++    // Remove the board from boardNames, and then using the path
++    // found in boardNames remove it from upstreamPorts and
++    // downstreamPorts.
++    auto boardFind = boardNames.find(boardName);
++    if (boardFind == boardNames.end())
++    {
++        return;
++    }
++
++    std::string boardPath = boardFind->second;
++
++    boardNames.erase(boardFind);
++
++    for (auto it = upstreamPorts.begin(); it != upstreamPorts.end();)
++    {
++        auto pathIt = std::find(it->second.begin(), it->second.end(),
++                                boardPath);
++        if (pathIt != it->second.end())
++        {
++            it->second.erase(pathIt);
++        }
++
++        if (it->second.empty())
++        {
++            it = upstreamPorts.erase(it);
++        }
++        else
++        {
++            ++it;
++        }
++    }
++
++    for (auto it = downstreamPorts.begin(); it != downstreamPorts.end();)
++    {
++        auto pathIt = std::find(it->second.begin(), it->second.end(),
++                                boardPath);
++        if (pathIt != it->second.end())
++        {
++            it->second.erase(pathIt);
++        }
++
++        if (it->second.empty())
++        {
++            it = downstreamPorts.erase(it);
++        }
++        else
++        {
++            ++it;
++        }
++    }
++}
+diff --git a/src/topology.hpp b/src/topology.hpp
+index 18c9244..4ea5246 100644
+--- a/src/topology.hpp
++++ b/src/topology.hpp
+@@ -13,15 +13,20 @@ class Topology
+     explicit Topology() = default;
+ 
+     void addBoard(const std::string& path, const std::string& boardType,
++                  const std::string& boardName,
+                   const nlohmann::json& exposesItem);
+-    std::unordered_map<std::string, std::vector<Association>> getAssocs();
++    std::unordered_map<std::string, std::vector<Association>>
++        getAssocs(const std::map<std::string, std::string>& boards);
++    void remove(const std::string& boardName);
+ 
+   private:
+     using Path = std::string;
+     using BoardType = std::string;
++    using BoardName = std::string;
+     using PortType = std::string;
+ 
+     std::unordered_map<PortType, std::vector<Path>> upstreamPorts;
+     std::unordered_map<PortType, std::vector<Path>> downstreamPorts;
+     std::unordered_map<Path, BoardType> boardTypes;
++    std::unordered_map<BoardName, Path> boardNames;
+ };
+diff --git a/test/test_topology.cpp b/test/test_topology.cpp
+index 76ab54a..f9beee3 100644
+--- a/test/test_topology.cpp
++++ b/test/test_topology.cpp
+@@ -35,11 +35,14 @@ const nlohmann::json otherExposesItem = nlohmann::json::parse(R"(
+     }
+ )");
+ 
++using BoardMap = std::map<std::string, std::string>;
++
+ TEST(Topology, Empty)
+ {
+     Topology topo;
++    BoardMap boards;
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -47,11 +50,12 @@ TEST(Topology, Empty)
+ TEST(Topology, EmptyExposes)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", nlohmann::json());
+-    topo.addBoard(superchassisPath, "Chassis", nlohmann::json());
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", nlohmann::json());
++    topo.addBoard(superchassisPath, "Chassis", "BoardB", nlohmann::json());
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -66,11 +70,14 @@ TEST(Topology, MissingConnectsTo)
+     )");
+ 
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisMissingConnectsTo);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA",
++                  subchassisMissingConnectsTo);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -78,11 +85,12 @@ TEST(Topology, MissingConnectsTo)
+ TEST(Topology, OtherExposes)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", otherExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", otherExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", otherExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB", otherExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -90,11 +98,13 @@ TEST(Topology, OtherExposes)
+ TEST(Topology, NoMatchSubchassis)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", otherExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", otherExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -102,11 +112,12 @@ TEST(Topology, NoMatchSubchassis)
+ TEST(Topology, NoMatchSuperchassis)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", otherExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB", otherExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 0);
+ }
+@@ -114,26 +125,48 @@ TEST(Topology, NoMatchSuperchassis)
+ TEST(Topology, Basic)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}, {superchassisPath, "BoardB"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 1);
+     EXPECT_EQ(assocs[subchassisPath].size(), 1);
+     EXPECT_EQ(assocs[subchassisPath][0], subchassisAssoc);
+ }
+ 
++TEST(Topology, NoNewBoards)
++{
++    Topology topo;
++    BoardMap boards;
++
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB",
++                  superchassisExposesItem);
++
++    // Boards A and B aren't new, so no assocs are returned.
++    auto assocs = topo.getAssocs(boards);
++
++    EXPECT_EQ(assocs.size(), 0);
++}
++
+ TEST(Topology, 2Subchassis)
+ {
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"},
++                    {subchassisPath + "2", "BoardB"},
++                    {superchassisPath, "BoardC"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+-    topo.addBoard(subchassisPath + "2", "Chassis", subchassisExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(subchassisPath + "2", "Chassis", "BoardB",
++                  subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardC",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 2);
+     EXPECT_EQ(assocs[subchassisPath].size(), 1);
+@@ -142,18 +175,42 @@ TEST(Topology, 2Subchassis)
+     EXPECT_EQ(assocs[subchassisPath + "2"][0], subchassisAssoc);
+ }
+ 
++TEST(Topology, OneNewBoard)
++{
++    Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"}};
++
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(subchassisPath + "2", "Chassis", "BoardB",
++                  subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardC",
++                  superchassisExposesItem);
++
++    // Only the assoc for BoardA should be returned
++    auto assocs = topo.getAssocs(boards);
++
++    EXPECT_EQ(assocs.size(), 1);
++    EXPECT_EQ(assocs[subchassisPath].size(), 1);
++    EXPECT_EQ(assocs[subchassisPath][0], subchassisAssoc);
++}
++
+ TEST(Topology, 2Superchassis)
+ {
+     const Association subchassisAssoc2 =
+         std::make_tuple("contained_by", "containing", superchassisPath + "2");
+ 
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"},
++                    {superchassisPath, "BoardB"},
++                    {superchassisPath + "2", "BoardC"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+-    topo.addBoard(superchassisPath + "2", "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardB",
++                  superchassisExposesItem);
++    topo.addBoard(superchassisPath + "2", "Chassis", "BoardC",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 1);
+     EXPECT_EQ(assocs[subchassisPath].size(), 2);
+@@ -168,13 +225,20 @@ TEST(Topology, 2SuperchassisAnd2Subchassis)
+         std::make_tuple("contained_by", "containing", superchassisPath + "2");
+ 
+     Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"},
++                    {subchassisPath + "2", "BoardB"},
++                    {superchassisPath, "BoardC"},
++                    {superchassisPath + "2", "BoardD"}};
+ 
+-    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+-    topo.addBoard(subchassisPath + "2", "Chassis", subchassisExposesItem);
+-    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+-    topo.addBoard(superchassisPath + "2", "Chassis", superchassisExposesItem);
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(subchassisPath + "2", "Chassis", "BoardB",
++                  subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardC",
++                  superchassisExposesItem);
++    topo.addBoard(superchassisPath + "2", "Chassis", "BoardD",
++                  superchassisExposesItem);
+ 
+-    auto assocs = topo.getAssocs();
++    auto assocs = topo.getAssocs(boards);
+ 
+     EXPECT_EQ(assocs.size(), 2);
+     EXPECT_EQ(assocs[subchassisPath].size(), 2);
+@@ -185,3 +249,43 @@ TEST(Topology, 2SuperchassisAnd2Subchassis)
+     EXPECT_THAT(assocs[subchassisPath + "2"],
+                 UnorderedElementsAre(subchassisAssoc, subchassisAssoc2));
+ }
++
++TEST(Topology, Remove)
++{
++    Topology topo;
++    BoardMap boards{{subchassisPath, "BoardA"},
++                    {subchassisPath + "2", "BoardB"},
++                    {superchassisPath, "BoardC"}};
++
++    topo.addBoard(subchassisPath, "Chassis", "BoardA", subchassisExposesItem);
++    topo.addBoard(subchassisPath + "2", "Chassis", "BoardB",
++                  subchassisExposesItem);
++    topo.addBoard(superchassisPath, "Chassis", "BoardC",
++                  superchassisExposesItem);
++
++    {
++        auto assocs = topo.getAssocs(boards);
++
++        EXPECT_EQ(assocs.size(), 2);
++        EXPECT_EQ(assocs[subchassisPath].size(), 1);
++        EXPECT_EQ(assocs[subchassisPath][0], subchassisAssoc);
++        EXPECT_EQ(assocs[subchassisPath + "2"].size(), 1);
++        EXPECT_EQ(assocs[subchassisPath + "2"][0], subchassisAssoc);
++    }
++
++    {
++        topo.remove("BoardA");
++        auto assocs = topo.getAssocs(boards);
++
++        EXPECT_EQ(assocs.size(), 1);
++        EXPECT_EQ(assocs[subchassisPath + "2"].size(), 1);
++        EXPECT_EQ(assocs[subchassisPath + "2"][0], subchassisAssoc);
++    }
++
++    {
++        topo.remove("BoardB");
++        auto assocs = topo.getAssocs(boards);
++
++        EXPECT_EQ(assocs.size(), 0);
++    }
++}
+-- 
+2.42.0.582.g8ccd20d70d-goog
+
diff --git a/recipes-phosphor/configuration/entity-manager/0001-topology-Add-support-for-LocationCode.patch b/recipes-phosphor/configuration/entity-manager/0001-topology-Add-support-for-LocationCode.patch
new file mode 100644
index 0000000..cc24fb1
--- /dev/null
+++ b/recipes-phosphor/configuration/entity-manager/0001-topology-Add-support-for-LocationCode.patch
@@ -0,0 +1,217 @@
+From 04f93ad2f4fbdc9a93f25162582a5a8a2b7ed6a1 Mon Sep 17 00:00:00 2001
+From: Willy Tu <wltu@google.com>
+Date: Fri, 29 Sep 2023 04:12:12 -0700
+Subject: [PATCH] topology: Add support for LocationCode
+
+Support setting `Label` in the upstream port to create the LocationCode
+dbus interface on the downstream port when they are connect to it.
+
+Tested:
+
+Upstream Port
+```
+    "Exposes": [
+        {
+            "Name": "Test Upstream Port",
+            "Label": "XYZ",
+            "Type": "Test Upstream Port"
+        }
+    ],
+```
+
+Downstream Port
+```
+    "Exposes": [
+        {
+            "Name": "Test Downstream Port",
+            "Type": "DownstreamPort",
+            "ConnectsToType": "Test Upstream Port"
+        }
+    ],
+```
+
+The LocationCode dbus interface is created on the downstream port with
+`XYZ` as the value.
+
+Patch Tracking Bug: b/302709931
+Upstream info / review:
+https://gerrit.openbmc.org/c/openbmc/entity-manager/+/66842
+Upstream-Status: Submitted
+Justification: New feature going into upstream for review.
+
+Change-Id: Ia63c01bc303fa3104a48720382bc899d5d0e5f9c
+Signed-off-by: Willy Tu <wltu@google.com>
+---
+ src/entity_manager.cpp | 22 +++++++++++++++--
+ src/topology.cpp       | 55 ++++++++++++++++++++++++++++++++++++++++++
+ src/topology.hpp       |  9 +++++++
+ 3 files changed, 84 insertions(+), 2 deletions(-)
+
+diff --git a/src/entity_manager.cpp b/src/entity_manager.cpp
+index d5844b9..bc72829 100644
+--- a/src/entity_manager.cpp
++++ b/src/entity_manager.cpp
+@@ -43,6 +43,7 @@
+ #include <functional>
+ #include <iostream>
+ #include <map>
++#include <optional>
+ #include <queue>
+ #include <regex>
+ #include <variant>
+@@ -884,6 +885,23 @@ void postToDbus(const nlohmann::json& newConfiguration,
+         ifacePtr->initialize();
+     }
+
++    for (const auto& [locationPath, locationPathValue] :
++         topology.getLocationCodes(newBoards))
++    {
++        auto findBoard = newBoards.find(locationPath);
++        if (findBoard == newBoards.end() || locationPathValue == std::nullopt)
++        {
++            continue;
++        }
++
++        auto locIntf = createInterface(
++            objServer, locationPath,
++            "xyz.openbmc_project.Inventory.Decorator.LocationCode",
++            findBoard->second);
++        locIntf->register_property("LocationCode", locationPathValue.value());
++        locIntf->initialize();
++    }
++
+     // All the DBus objects and interfaces are created. Try to construct entity
+     // inventory objects association based on I2C topology
+     // Node -> entity object path, entity name, entity type indicating entites
+@@ -924,8 +942,8 @@ void postToDbus(const nlohmann::json& newConfiguration,
+             for (const auto& slot : i2cSlots[i2c])
+             {
+                 auto slotPath = std::get<0>(slot);
+-                auto entityPath =
+-                    slotPath.substr(0, slotPath.find_last_of('/'));
++                auto entityPath = slotPath.substr(0,
++                                                  slotPath.find_last_of('/'));
+                 Node connectedEntity = std::make_tuple(
+                     entityPath, std::get<1>(slot), std::get<2>(slot));
+
+diff --git a/src/topology.cpp b/src/topology.cpp
+index f2ee22b..54af2d1 100644
+--- a/src/topology.cpp
++++ b/src/topology.cpp
+@@ -1,6 +1,7 @@
+ #include "topology.hpp"
+
+ #include <iostream>
++#include <optional>
+
+ void Topology::addBoard(const std::string& path, const std::string& boardType,
+                         const std::string& boardName,
+@@ -34,6 +35,13 @@ void Topology::addBoard(const std::string& path, const std::string& boardType,
+     {
+         upstreamPorts[exposesType].emplace_back(path);
+         boardTypes[path] = boardType;
++
++        auto findLocationCode = exposesItem.find("Label");
++        if (findLocationCode != exposesItem.end())
++        {
++            locationCodes[exposesType][path] =
++                findLocationCode->get<std::string>();
++        }
+     }
+ }
+
+@@ -74,6 +82,53 @@ std::unordered_map<std::string, std::vector<Association>>
+     return result;
+ }
+
++std::unordered_map<std::string, std::optional<std::string>>
++    Topology::getLocationCodes(const std::map<std::string, std::string>& boards)
++{
++    std::unordered_map<std::string, std::optional<std::string>> result;
++
++    // look at each upstream port type
++    for (const auto& upstreamPortPair : upstreamPorts)
++    {
++        auto downstreamMatch = downstreamPorts.find(upstreamPortPair.first);
++
++        if (downstreamMatch == downstreamPorts.end())
++        {
++            // no match
++            continue;
++        }
++        auto locationCodeMatch = locationCodes.find(upstreamPortPair.first);
++        if (locationCodeMatch == locationCodes.end())
++        {
++            // no match
++            continue;
++        }
++
++        for (const Path& upstream : upstreamPortPair.second)
++        {
++            if (boardTypes[upstream] == "Chassis" ||
++                boardTypes[upstream] == "Board")
++            {
++                for (const Path& downstream : downstreamMatch->second)
++                {
++                    // The downstream path must be one we care about.
++                    if (boards.find(downstream) != boards.end())
++                    {
++                        auto findLocationCode =
++                            locationCodeMatch->second.find(upstream);
++                        if (findLocationCode != locationCodeMatch->second.end())
++                        {
++                            result[downstream] = findLocationCode->second;
++                        }
++                    }
++                }
++            }
++        }
++    }
++
++    return result;
++}
++
+ void Topology::remove(const std::string& boardName)
+ {
+     // Remove the board from boardNames, and then using the path
+diff --git a/src/topology.hpp b/src/topology.hpp
+index 4ea5246..4742646 100644
+--- a/src/topology.hpp
++++ b/src/topology.hpp
+@@ -2,7 +2,9 @@
+
+ #include <nlohmann/json.hpp>
+
++#include <optional>
+ #include <set>
++#include <string>
+ #include <unordered_map>
+
+ using Association = std::tuple<std::string, std::string, std::string>;
+@@ -17,16 +19,23 @@ class Topology
+                   const nlohmann::json& exposesItem);
+     std::unordered_map<std::string, std::vector<Association>>
+         getAssocs(const std::map<std::string, std::string>& boards);
++
++    std::unordered_map<std::string, std::optional<std::string>>
++        getLocationCodes(const std::map<std::string, std::string>& boards);
++
+     void remove(const std::string& boardName);
+
+   private:
+     using Path = std::string;
+     using BoardType = std::string;
++    using LocationCode = std::string;
+     using BoardName = std::string;
+     using PortType = std::string;
+
+     std::unordered_map<PortType, std::vector<Path>> upstreamPorts;
+     std::unordered_map<PortType, std::vector<Path>> downstreamPorts;
+     std::unordered_map<Path, BoardType> boardTypes;
++    std::unordered_map<PortType, std::unordered_map<Path, LocationCode>>
++        locationCodes;
+     std::unordered_map<BoardName, Path> boardNames;
+ };
+--
+2.42.0.582.g8ccd20d70d-goog
+
diff --git a/recipes-phosphor/configuration/entity-manager/0002-entity-manager-Build-entity-inventory-association.patch b/recipes-phosphor/configuration/entity-manager/0002-entity-manager-Build-entity-inventory-association.patch
index 8c83f5b..dbfc30a 100644
--- a/recipes-phosphor/configuration/entity-manager/0002-entity-manager-Build-entity-inventory-association.patch
+++ b/recipes-phosphor/configuration/entity-manager/0002-entity-manager-Build-entity-inventory-association.patch
@@ -1,4 +1,4 @@
-From a929032b0f3fd72dec266458ed3479c54bc1f7c4 Mon Sep 17 00:00:00 2001
+From 571da721e01281146ee19648b4002c5ac5b9a8d6 Mon Sep 17 00:00:00 2001
 From: Josh Lehan <krellan@google.com>
 Date: Wed, 18 May 2022 15:24:02 -0700
 Subject: [PATCH] entity-manager: Build entity inventory association
@@ -67,7 +67,7 @@
  4 files changed, 262 insertions(+), 27 deletions(-)
 
 diff --git a/src/entity_manager.cpp b/src/entity_manager.cpp
-index f0e1c96..3c6a122 100644
+index 621d97a..d5844b9 100644
 --- a/src/entity_manager.cpp
 +++ b/src/entity_manager.cpp
 @@ -43,6 +43,7 @@
@@ -80,7 +80,7 @@
  constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations";
 @@ -569,6 +570,26 @@ void postToDbus(const nlohmann::json& newConfiguration,
  {
-     Topology topology;
+     std::map<std::string, std::string> newBoards; // path -> name
  
 +    // I2C bus -> {(entity path, entity name, entity type)}
 +    // I2C bus is the entity EEPROM I2C bus that is usually probed from
@@ -149,8 +149,8 @@
              std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
                  createInterface(objServer, ifacePath,
                                  "xyz.openbmc_project.Configuration." + itemType,
-@@ -785,6 +836,184 @@ void postToDbus(const nlohmann::json& newConfiguration,
-         ifacePtr->register_property("Associations", boardAssoc.second);
+@@ -832,6 +883,184 @@ void postToDbus(const nlohmann::json& newConfiguration,
+         ifacePtr->register_property("Associations", assocPropValue);
          ifacePtr->initialize();
      }
 +
@@ -335,7 +335,7 @@
  
  // reads json files out of the filesystem
 diff --git a/src/fru_device.cpp b/src/fru_device.cpp
-index c67bfc5..ec6fc4f 100644
+index ba341d5..b419799 100644
 --- a/src/fru_device.cpp
 +++ b/src/fru_device.cpp
 @@ -141,33 +141,6 @@ static int busStrToInt(const std::string_view busName)
@@ -373,10 +373,10 @@
                                 sdbusplus::asio::object_server& objServer)
  {
 diff --git a/src/perform_scan.cpp b/src/perform_scan.cpp
-index 0ad6509..255528b 100644
+index 2be6477..a7476c8 100644
 --- a/src/perform_scan.cpp
 +++ b/src/perform_scan.cpp
-@@ -614,6 +614,12 @@ void PerformScan::run()
+@@ -610,6 +610,12 @@ void PerformScan::run()
              continue;
          }
  
@@ -390,10 +390,10 @@
          nlohmann::json probeCommand;
          if ((*findProbe).type() != nlohmann::json::value_t::array)
 diff --git a/src/utils.hpp b/src/utils.hpp
-index 0f51980..961c768 100644
+index 746bc25..d254d97 100644
 --- a/src/utils.hpp
 +++ b/src/utils.hpp
-@@ -167,3 +167,30 @@ inline bool deviceHasLogging(const nlohmann::json& json)
+@@ -160,3 +160,30 @@ inline bool deviceHasLogging(const nlohmann::json& json)
  /// \param dbusValue the property value being matched to a probe.
  /// \return true if the dbusValue matched the probe otherwise false.
  bool matchProbe(const nlohmann::json& probe, const DBusValueVariant& dbusValue);
@@ -425,5 +425,5 @@
 +        "/sys/bus/i2c/devices/i2c-" + std::to_string(bus) + "/mux_device"));
 +}
 -- 
-2.40.0.348.gf938b09366-goog
+2.42.0.582.g8ccd20d70d-goog
 
diff --git a/recipes-phosphor/configuration/entity-manager_%.bbappend b/recipes-phosphor/configuration/entity-manager_%.bbappend
index 7a2b806..c202328 100644
--- a/recipes-phosphor/configuration/entity-manager_%.bbappend
+++ b/recipes-phosphor/configuration/entity-manager_%.bbappend
@@ -2,12 +2,14 @@
 
 SRC_URI:append:gbmc = " \
     file://0001-Create-Associations-From-Defined-Property.patch \
-    file://0002-entity-manager-Build-entity-inventory-association.patch \
     file://0001-fru_device-limit-the-fru-scan-range-to-0x50-0x57.patch \
     file://0005-entity-manager-Avoid-direct-substitution.patch \
     file://0006-entity-manager-Allow-substituting-variables-more-tha.patch \
     file://0001-Add-USB-Device-discovery-daemon.patch \
     file://0001-Increasing-the-bus-read-timeout-for-FRU.patch \
+    file://0001-phys-topology-Add-late-add-remove-support.patch \
+    file://0002-entity-manager-Build-entity-inventory-association.patch \
+    file://0001-topology-Add-support-for-LocationCode.patch \
 "
 SYSTEMD_AUTO_ENABLE:usb-device = "enable"
 SYSTEMD_SERVICE:usb-device = "xyz.openbmc_project.UsbDevice.service"