Check for pending associations

When the mapper is adding a new D-Bus path to its path map,
either via an introspect or in the interfacesAdded handler,
check if that new path has an outstanding pending association.

If it does, then create the 2 real association paths and
remove that entry from the pending associations maps.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I2da8109b5cba8596eb0c14a6af0d377472ca4145
diff --git a/src/associations.cpp b/src/associations.cpp
index 30a8e9a..f9aa6dc 100644
--- a/src/associations.cpp
+++ b/src/associations.cpp
@@ -143,6 +143,38 @@
     }
 }
 
+void addEndpointsToAssocIfaces(
+    sdbusplus::asio::object_server& objectServer, const std::string& assocPath,
+    const boost::container::flat_set<std::string>& endpointPaths,
+    AssociationMaps& assocMaps)
+{
+    auto& iface = assocMaps.ifaces[assocPath];
+    auto& i = std::get<ifacePos>(iface);
+    auto& endpoints = std::get<endpointsPos>(iface);
+
+    // Only add new endpoints
+    for (auto& e : endpointPaths)
+    {
+        if (std::find(endpoints.begin(), endpoints.end(), e) == endpoints.end())
+        {
+            endpoints.push_back(e);
+        }
+    }
+
+    // If the interface already exists, only need to update
+    // the property value, otherwise create it
+    if (i)
+    {
+        i->set_property("endpoints", endpoints);
+    }
+    else
+    {
+        i = objectServer.add_interface(assocPath, XYZ_ASSOCIATION_INTERFACE);
+        i->register_property("endpoints", endpoints);
+        i->initialize();
+    }
+}
+
 void associationChanged(sdbusplus::asio::object_server& objectServer,
                         const std::vector<Association>& associations,
                         const std::string& path, const std::string& owner,
@@ -183,36 +215,8 @@
     }
     for (const auto& object : objects)
     {
-        // the mapper exposes the new association interface but intakes
-        // the old
-
-        auto& iface = assocMaps.ifaces[object.first];
-        auto& i = std::get<ifacePos>(iface);
-        auto& endpoints = std::get<endpointsPos>(iface);
-
-        // Only add new endpoints
-        for (auto& e : object.second)
-        {
-            if (std::find(endpoints.begin(), endpoints.end(), e) ==
-                endpoints.end())
-            {
-                endpoints.push_back(e);
-            }
-        }
-
-        // If the interface already exists, only need to update
-        // the property value, otherwise create it
-        if (i)
-        {
-            i->set_property("endpoints", endpoints);
-        }
-        else
-        {
-            i = objectServer.add_interface(object.first,
-                                           XYZ_ASSOCIATION_INTERFACE);
-            i->register_property("endpoints", endpoints);
-            i->initialize();
-        }
+        addEndpointsToAssocIfaces(objectServer, object.first, object.second,
+                                  assocMaps);
     }
 
     // Check for endpoints being removed instead of added
@@ -305,3 +309,107 @@
         assoc++;
     }
 }
+
+void addSingleAssociation(sdbusplus::asio::object_server& server,
+                          const std::string& assocPath,
+                          const std::string& endpoint, const std::string& owner,
+                          const std::string& ownerPath,
+                          AssociationMaps& assocMaps)
+{
+    boost::container::flat_set<std::string> endpoints{endpoint};
+
+    addEndpointsToAssocIfaces(server, assocPath, endpoints, assocMaps);
+
+    AssociationPaths objects;
+    boost::container::flat_set e{endpoint};
+    objects.emplace(assocPath, e);
+
+    auto a = assocMaps.owners.find(ownerPath);
+    if (a != assocMaps.owners.end())
+    {
+        auto o = a->second.find(owner);
+        if (o != a->second.end())
+        {
+            auto p = o->second.find(assocPath);
+            if (p != o->second.end())
+            {
+                p->second.emplace(endpoint);
+            }
+            else
+            {
+                o->second.emplace(assocPath, e);
+            }
+        }
+        else
+        {
+            a->second.emplace(owner, std::move(objects));
+        }
+    }
+    else
+    {
+        boost::container::flat_map<std::string, AssociationPaths> owners;
+        owners.emplace(owner, std::move(objects));
+        assocMaps.owners.emplace(endpoint, owners);
+    }
+}
+
+void checkIfPendingAssociation(const std::string& objectPath,
+                               const interface_map_type& interfaceMap,
+                               AssociationMaps& assocMaps,
+                               sdbusplus::asio::object_server& server)
+{
+    auto pending = assocMaps.pending.find(objectPath);
+    if (pending == assocMaps.pending.end())
+    {
+        return;
+    }
+
+    if (interfaceMap.find(objectPath) == interfaceMap.end())
+    {
+        return;
+    }
+
+    auto endpoint = pending->second.begin();
+
+    while (endpoint != pending->second.end())
+    {
+        const auto& e = std::get<assocPos>(*endpoint);
+
+        // Ensure the other side of the association still exists
+        if (interfaceMap.find(std::get<reversePathPos>(e)) ==
+            interfaceMap.end())
+        {
+            endpoint++;
+            continue;
+        }
+
+        // Add both sides of the association:
+        //  objectPath/forwardType and reversePath/reverseType
+        //
+        // The ownerPath is the reversePath - i.e. the endpoint that
+        // is on D-Bus and owns the org.openbmc.Associations iface.
+        //
+        const auto& ownerPath = std::get<reversePathPos>(e);
+        const auto& owner = std::get<ownerPos>(*endpoint);
+
+        auto assocPath = objectPath + '/' + std::get<forwardTypePos>(e);
+        auto endpointPath = ownerPath;
+
+        addSingleAssociation(server, assocPath, endpointPath, owner, ownerPath,
+                             assocMaps);
+
+        // Now the reverse direction (still the same owner and ownerPath)
+        assocPath = endpointPath + '/' + std::get<reverseTypePos>(e);
+        endpointPath = objectPath;
+        addSingleAssociation(server, assocPath, endpointPath, owner, ownerPath,
+                             assocMaps);
+
+        // Not pending anymore
+        endpoint = pending->second.erase(endpoint);
+    }
+
+    if (pending->second.empty())
+    {
+        assocMaps.pending.erase(objectPath);
+    }
+}
diff --git a/src/associations.hpp b/src/associations.hpp
index 3f4b81e..60342b2 100644
--- a/src/associations.hpp
+++ b/src/associations.hpp
@@ -120,3 +120,35 @@
  */
 void removeFromPendingAssociations(const std::string& endpointPath,
                                    AssociationMaps& assocMaps);
+
+/** @brief Adds a single association D-Bus object (<path>/<type>)
+ *
+ * @param[in,out] server    - sdbus system object
+ * @param[in] assocPath     - The association D-Bus path
+ * @param[in] endpoint      - The association's D-Bus endpoint path
+ * @param[in] owner         - The owning D-Bus well known name
+ * @param[in] ownerPath     - The D-Bus path hosting the Associations property
+ * @param[in,out] assocMaps - The association maps
+ */
+void addSingleAssociation(sdbusplus::asio::object_server& server,
+                          const std::string& assocPath,
+                          const std::string& endpoint, const std::string& owner,
+                          const std::string& ownerPath,
+                          AssociationMaps& assocMaps);
+
+/** @brief Create a real association out of a pending association
+ *         if there is one for this path.
+ *
+ * If objectPath is now on D-Bus, and it is also in the pending associations
+ * map, create the 2 real association objects and remove its pending
+ * associations entry.  Used when a new path shows up in D-Bus.
+ *
+ * @param[in] objectPath    - the path to check
+ * @param[in] interfaceMap  - The master interface map
+ * @param[in,out] assocMaps - The association maps
+ * @param[in,out] server    - sdbus system object
+ */
+void checkIfPendingAssociation(const std::string& objectPath,
+                               const interface_map_type& interfaceMap,
+                               AssociationMaps& assocMaps,
+                               sdbusplus::asio::object_server& server);
diff --git a/src/main.cpp b/src/main.cpp
index 49b1344..84d5b67 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -81,7 +81,7 @@
 {
     InProgressIntrospect(
         sdbusplus::asio::connection* system_bus, boost::asio::io_service& io,
-        const std::string& process_name
+        const std::string& process_name, AssociationMaps& am
 #ifdef DEBUG
         ,
         std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>>
@@ -89,7 +89,7 @@
 #endif
         ) :
         system_bus(system_bus),
-        io(io), process_name(process_name)
+        io(io), process_name(process_name), assocMaps(am)
 #ifdef DEBUG
         ,
         global_start_time(global_start_time),
@@ -120,6 +120,7 @@
     sdbusplus::asio::connection* system_bus;
     boost::asio::io_service& io;
     std::string process_name;
+    AssociationMaps& assocMaps;
 #ifdef DEBUG
     std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>>
         global_start_time;
@@ -209,6 +210,11 @@
                 pElement = pElement->NextSiblingElement("interface");
             }
 
+            // Check if this new path has a pending association that can
+            // now be completed.
+            checkIfPendingAssociation(path, interface_map,
+                                      transaction->assocMaps, objectServer);
+
             pElement = pRoot->FirstChildElement("node");
             while (pElement != nullptr)
             {
@@ -234,6 +240,7 @@
 void start_new_introspect(
     sdbusplus::asio::connection* system_bus, boost::asio::io_service& io,
     interface_map_type& interface_map, const std::string& process_name,
+    AssociationMaps& assocMaps,
 #ifdef DEBUG
     std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>>
         global_start_time,
@@ -243,7 +250,8 @@
     if (needToIntrospect(process_name, service_whitelist, service_blacklist))
     {
         std::shared_ptr<InProgressIntrospect> transaction =
-            std::make_shared<InProgressIntrospect>(system_bus, io, process_name
+            std::make_shared<InProgressIntrospect>(system_bus, io, process_name,
+                                                   assocMaps
 #ifdef DEBUG
                                                    ,
                                                    global_start_time
@@ -280,11 +288,11 @@
     boost::asio::io_service& io, interface_map_type& interface_map,
     sdbusplus::asio::connection* system_bus,
     boost::container::flat_map<std::string, std::string>& name_owners,
-    sdbusplus::asio::object_server& objectServer)
+    AssociationMaps& assocMaps, sdbusplus::asio::object_server& objectServer)
 {
     system_bus->async_method_call(
-        [&io, &interface_map, &name_owners, &objectServer,
-         system_bus](const boost::system::error_code ec,
+        [&io, &interface_map, &name_owners, &objectServer, system_bus,
+         &assocMaps](const boost::system::error_code ec,
                      std::vector<std::string> process_names) {
             if (ec)
             {
@@ -306,7 +314,7 @@
                                      service_blacklist))
                 {
                     start_new_introspect(system_bus, io, interface_map,
-                                         process_name,
+                                         process_name, assocMaps,
 #ifdef DEBUG
                                          global_start_time,
 #endif
@@ -481,7 +489,7 @@
                 {
                     name_owners[new_owner] = name;
                     start_new_introspect(system_bus.get(), io, interface_map,
-                                         name,
+                                         name, associationMaps,
 #ifdef DEBUG
                                          transaction,
 #endif
@@ -828,7 +836,8 @@
     iface->initialize();
 
     io.post([&]() {
-        doListNames(io, interface_map, system_bus.get(), name_owners, server);
+        doListNames(io, interface_map, system_bus.get(), name_owners,
+                    associationMaps, server);
     });
 
     io.run();
diff --git a/src/processing.cpp b/src/processing.cpp
index 4125389..d3b7a76 100644
--- a/src/processing.cpp
+++ b/src/processing.cpp
@@ -165,4 +165,7 @@
 
         pos = parent.find_last_of('/');
     }
+
+    // The new interface might have an association pending
+    checkIfPendingAssociation(objPath.str, interfaceMap, assocMaps, server);
 }
diff --git a/src/test/associations.cpp b/src/test/associations.cpp
index 5f87020..dee991d 100644
--- a/src/test/associations.cpp
+++ b/src/test/associations.cpp
@@ -444,3 +444,30 @@
     removeFromPendingAssociations(DEFAULT_ENDPOINT, assocMaps);
     EXPECT_EQ(assocMaps.pending.size(), 0);
 }
+
+// Test moving a pending association to a real one
+TEST_F(TestAssociations, checkIfPending)
+{
+    AssociationMaps assocMaps;
+    interface_map_type interfaceMap = {
+        {DEFAULT_SOURCE_PATH, {{DEFAULT_DBUS_SVC, {"a"}}}},
+        {DEFAULT_ENDPOINT, {{DEFAULT_DBUS_SVC, {"b"}}}}};
+
+    addPendingAssociation(DEFAULT_SOURCE_PATH, "inventory", DEFAULT_ENDPOINT,
+                          "error", DEFAULT_DBUS_SVC, assocMaps);
+    EXPECT_EQ(assocMaps.pending.size(), 1);
+
+    // Move the pending association to a real association
+    checkIfPendingAssociation(DEFAULT_SOURCE_PATH, interfaceMap, assocMaps,
+                              *server);
+
+    EXPECT_TRUE(assocMaps.pending.empty());
+    EXPECT_EQ(assocMaps.owners.size(), 1);
+    EXPECT_EQ(assocMaps.ifaces.size(), 2);
+
+    // This shouldn't do anything, since /new/path isn't pending
+    checkIfPendingAssociation("/new/path", interfaceMap, assocMaps, *server);
+    EXPECT_TRUE(assocMaps.pending.empty());
+    EXPECT_EQ(assocMaps.owners.size(), 1);
+    EXPECT_EQ(assocMaps.ifaces.size(), 2);
+}