1 #include "associations.hpp"
2 
3 #include <boost/asio/steady_timer.hpp>
4 #include <sdbusplus/exception.hpp>
5 
6 #include <iostream>
7 #include <string>
8 
9 void updateEndpointsOnDbus(sdbusplus::asio::object_server& objectServer,
10                            const std::string& assocPath,
11                            AssociationMaps& assocMaps)
12 {
13     auto& iface = assocMaps.ifaces[assocPath];
14     auto& i = std::get<ifacePos>(iface);
15     auto& endpoints = std::get<endpointsPos>(iface);
16 
17     // If the interface already exists, only need to update
18     // the property value, otherwise create it
19     if (i)
20     {
21         if (endpoints.empty())
22         {
23             objectServer.remove_interface(i);
24             i = nullptr;
25         }
26         else
27         {
28             i->set_property("endpoints", endpoints);
29         }
30     }
31     else
32     {
33         if (!endpoints.empty())
34         {
35             i = objectServer.add_interface(assocPath, xyzAssociationInterface);
36             i->register_property("endpoints", endpoints);
37             i->initialize();
38         }
39     }
40 }
41 
42 void scheduleUpdateEndpointsOnDbus(boost::asio::io_context& io,
43                                    sdbusplus::asio::object_server& objectServer,
44                                    const std::string& assocPath,
45                                    AssociationMaps& assocMaps)
46 {
47     static std::set<std::string> delayedUpdatePaths;
48 
49     if (delayedUpdatePaths.contains(assocPath))
50     {
51         return;
52     }
53 
54     auto& iface = assocMaps.ifaces[assocPath];
55     auto& endpoints = std::get<endpointsPos>(iface);
56 
57     if (endpoints.size() > endpointsCountTimerThreshold)
58     {
59         delayedUpdatePaths.emplace(assocPath);
60         auto timer = std::make_shared<boost::asio::steady_timer>(
61             io, std::chrono::seconds(endpointUpdateDelaySeconds));
62         timer->async_wait([&objectServer, &assocMaps, timer,
63                            assocPath](const boost::system::error_code& ec) {
64             if (!ec)
65             {
66                 updateEndpointsOnDbus(objectServer, assocPath, assocMaps);
67             }
68             delayedUpdatePaths.erase(assocPath);
69         });
70     }
71     else
72     {
73         updateEndpointsOnDbus(objectServer, assocPath, assocMaps);
74     }
75 }
76 
77 void removeAssociation(boost::asio::io_context& io,
78                        const std::string& sourcePath, const std::string& owner,
79                        sdbusplus::asio::object_server& server,
80                        AssociationMaps& assocMaps)
81 {
82     // Use associationOwners to find the association paths and endpoints
83     // that the passed in object path and service own.  Remove all of
84     // these endpoints from the actual association D-Bus objects, and if
85     // the endpoints property is then empty, the whole association object
86     // can be removed.  Note there can be multiple services that own an
87     // association, and also that sourcePath is the path of the object
88     // that contains the org.openbmc.Associations interface and not the
89     // association path itself.
90 
91     // Find the services that have associations for this object path
92     auto owners = assocMaps.owners.find(sourcePath);
93     if (owners == assocMaps.owners.end())
94     {
95         return;
96     }
97 
98     // Find the association paths and endpoints owned by this object
99     // path for this service.
100     auto assocs = owners->second.find(owner);
101     if (assocs == owners->second.end())
102     {
103         return;
104     }
105 
106     for (const auto& [assocPath, endpointsToRemove] : assocs->second)
107     {
108         removeAssociationEndpoints(io, server, assocPath, endpointsToRemove,
109                                    assocMaps);
110     }
111 
112     // Remove the associationOwners entries for this owning path/service.
113     owners->second.erase(assocs);
114     if (owners->second.empty())
115     {
116         assocMaps.owners.erase(owners);
117     }
118 
119     // If we were still waiting on the other side of this association to
120     // show up, cancel that wait.
121     removeFromPendingAssociations(sourcePath, assocMaps);
122 }
123 
124 void removeAssociationEndpoints(
125     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
126     const std::string& assocPath,
127     const boost::container::flat_set<std::string>& endpointsToRemove,
128     AssociationMaps& assocMaps)
129 {
130     auto assoc = assocMaps.ifaces.find(assocPath);
131     if (assoc == assocMaps.ifaces.end())
132     {
133         return;
134     }
135 
136     auto& endpointsInDBus = std::get<endpointsPos>(assoc->second);
137 
138     for (const auto& endpointToRemove : endpointsToRemove)
139     {
140         auto e = std::find(endpointsInDBus.begin(), endpointsInDBus.end(),
141                            endpointToRemove);
142 
143         if (e != endpointsInDBus.end())
144         {
145             endpointsInDBus.erase(e);
146         }
147     }
148 
149     scheduleUpdateEndpointsOnDbus(io, objectServer, assocPath, assocMaps);
150 }
151 
152 void checkAssociationEndpointRemoves(
153     boost::asio::io_context& io, const std::string& sourcePath,
154     const std::string& owner, const AssociationPaths& newAssociations,
155     sdbusplus::asio::object_server& objectServer, AssociationMaps& assocMaps)
156 {
157     // Find the services that have associations on this path.
158     auto originalOwners = assocMaps.owners.find(sourcePath);
159     if (originalOwners == assocMaps.owners.end())
160     {
161         return;
162     }
163 
164     // Find the associations for this service
165     auto originalAssociations = originalOwners->second.find(owner);
166     if (originalAssociations == originalOwners->second.end())
167     {
168         return;
169     }
170 
171     // Compare the new endpoints versus the original endpoints, and
172     // remove any of the original ones that aren't in the new list.
173     for (const auto& [originalAssocPath, originalEndpoints] :
174          originalAssociations->second)
175     {
176         // Check if this source even still has each association that
177         // was there previously, and if not, remove all of its endpoints
178         // from the D-Bus endpoints property which will cause the whole
179         // association path to be removed if no endpoints remain.
180         auto newEndpoints = newAssociations.find(originalAssocPath);
181         if (newEndpoints == newAssociations.end())
182         {
183             removeAssociationEndpoints(io, objectServer, originalAssocPath,
184                                        originalEndpoints, assocMaps);
185         }
186         else
187         {
188             // The association is still there.  Check if the endpoints
189             // changed.
190             boost::container::flat_set<std::string> toRemove;
191 
192             for (const auto& originalEndpoint : originalEndpoints)
193             {
194                 if (std::find(newEndpoints->second.begin(),
195                               newEndpoints->second.end(),
196                               originalEndpoint) == newEndpoints->second.end())
197                 {
198                     toRemove.emplace(originalEndpoint);
199                 }
200             }
201             if (!toRemove.empty())
202             {
203                 removeAssociationEndpoints(io, objectServer, originalAssocPath,
204                                            toRemove, assocMaps);
205             }
206         }
207     }
208 }
209 
210 void addEndpointsToAssocIfaces(
211     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
212     const std::string& assocPath,
213     const boost::container::flat_set<std::string>& endpointPaths,
214     AssociationMaps& assocMaps)
215 {
216     auto& iface = assocMaps.ifaces[assocPath];
217     auto& endpoints = std::get<endpointsPos>(iface);
218 
219     // Only add new endpoints
220     for (const auto& e : endpointPaths)
221     {
222         if (std::find(endpoints.begin(), endpoints.end(), e) == endpoints.end())
223         {
224             endpoints.push_back(e);
225         }
226     }
227     scheduleUpdateEndpointsOnDbus(io, objectServer, assocPath, assocMaps);
228 }
229 
230 void associationChanged(boost::asio::io_context& io,
231                         sdbusplus::asio::object_server& objectServer,
232                         const std::vector<Association>& associations,
233                         const std::string& path, const std::string& owner,
234                         const InterfaceMapType& interfaceMap,
235                         AssociationMaps& assocMaps)
236 {
237     AssociationPaths objects;
238 
239     for (const Association& association : associations)
240     {
241         std::string forward;
242         std::string reverse;
243         std::string objectPath;
244         std::tie(forward, reverse, objectPath) = association;
245 
246         if (objectPath.empty())
247         {
248             std::cerr << "Found invalid association on path " << path << "\n";
249             continue;
250         }
251 
252         // Can't create this association if the endpoint isn't on D-Bus.
253         if (interfaceMap.find(objectPath) == interfaceMap.end())
254         {
255             addPendingAssociation(objectPath, reverse, path, forward, owner,
256                                   assocMaps);
257             continue;
258         }
259 
260         if (!forward.empty())
261         {
262             objects[path + "/" + forward].emplace(objectPath);
263         }
264         if (!reverse.empty())
265         {
266             objects[objectPath + "/" + reverse].emplace(path);
267         }
268     }
269     for (const auto& object : objects)
270     {
271         addEndpointsToAssocIfaces(io, objectServer, object.first, object.second,
272                                   assocMaps);
273     }
274 
275     // Check for endpoints being removed instead of added
276     checkAssociationEndpointRemoves(io, path, owner, objects, objectServer,
277                                     assocMaps);
278 
279     if (!objects.empty())
280     {
281         // Update associationOwners with the latest info
282         auto a = assocMaps.owners.find(path);
283         if (a != assocMaps.owners.end())
284         {
285             auto o = a->second.find(owner);
286             if (o != a->second.end())
287             {
288                 o->second = std::move(objects);
289             }
290             else
291             {
292                 a->second.emplace(owner, std::move(objects));
293             }
294         }
295         else
296         {
297             boost::container::flat_map<std::string, AssociationPaths> owners;
298             owners.emplace(owner, std::move(objects));
299             assocMaps.owners.emplace(path, owners);
300         }
301     }
302 }
303 
304 void addPendingAssociation(const std::string& objectPath,
305                            const std::string& type,
306                            const std::string& endpointPath,
307                            const std::string& endpointType,
308                            const std::string& owner, AssociationMaps& assocMaps)
309 {
310     Association assoc{type, endpointType, endpointPath};
311 
312     auto p = assocMaps.pending.find(objectPath);
313     if (p == assocMaps.pending.end())
314     {
315         ExistingEndpoints ee;
316         ee.emplace_back(owner, std::move(assoc));
317         assocMaps.pending.emplace(objectPath, std::move(ee));
318     }
319     else
320     {
321         // Already waiting on this path for another association,
322         // so just add this endpoint and owner.
323         auto& endpoints = p->second;
324         auto e = std::find_if(endpoints.begin(), endpoints.end(),
325                               [&assoc, &owner](const auto& endpoint) {
326             return (std::get<ownerPos>(endpoint) == owner) &&
327                    (std::get<assocPos>(endpoint) == assoc);
328         });
329         if (e == endpoints.end())
330         {
331             endpoints.emplace_back(owner, std::move(assoc));
332         }
333     }
334 }
335 
336 void removeFromPendingAssociations(const std::string& endpointPath,
337                                    AssociationMaps& assocMaps)
338 {
339     auto assoc = assocMaps.pending.begin();
340     while (assoc != assocMaps.pending.end())
341     {
342         auto endpoint = assoc->second.begin();
343         while (endpoint != assoc->second.end())
344         {
345             auto& e = std::get<assocPos>(*endpoint);
346             if (std::get<reversePathPos>(e) == endpointPath)
347             {
348                 endpoint = assoc->second.erase(endpoint);
349                 continue;
350             }
351 
352             endpoint++;
353         }
354 
355         if (assoc->second.empty())
356         {
357             assoc = assocMaps.pending.erase(assoc);
358             continue;
359         }
360 
361         assoc++;
362     }
363 }
364 
365 void addSingleAssociation(boost::asio::io_context& io,
366                           sdbusplus::asio::object_server& server,
367                           const std::string& assocPath,
368                           const std::string& endpoint, const std::string& owner,
369                           const std::string& ownerPath,
370                           AssociationMaps& assocMaps)
371 {
372     boost::container::flat_set<std::string> endpoints{endpoint};
373 
374     addEndpointsToAssocIfaces(io, server, assocPath, endpoints, assocMaps);
375 
376     AssociationPaths objects;
377     boost::container::flat_set e{endpoint};
378     objects.emplace(assocPath, e);
379 
380     auto a = assocMaps.owners.find(ownerPath);
381     if (a != assocMaps.owners.end())
382     {
383         auto o = a->second.find(owner);
384         if (o != a->second.end())
385         {
386             auto p = o->second.find(assocPath);
387             if (p != o->second.end())
388             {
389                 p->second.emplace(endpoint);
390             }
391             else
392             {
393                 o->second.emplace(assocPath, e);
394             }
395         }
396         else
397         {
398             a->second.emplace(owner, std::move(objects));
399         }
400     }
401     else
402     {
403         boost::container::flat_map<std::string, AssociationPaths> owners;
404         owners.emplace(owner, std::move(objects));
405         assocMaps.owners.emplace(endpoint, owners);
406     }
407 }
408 
409 void checkIfPendingAssociation(boost::asio::io_context& io,
410                                const std::string& objectPath,
411                                const InterfaceMapType& interfaceMap,
412                                AssociationMaps& assocMaps,
413                                sdbusplus::asio::object_server& server)
414 {
415     auto pending = assocMaps.pending.find(objectPath);
416     if (pending == assocMaps.pending.end())
417     {
418         return;
419     }
420 
421     if (interfaceMap.find(objectPath) == interfaceMap.end())
422     {
423         return;
424     }
425 
426     auto endpoint = pending->second.begin();
427 
428     while (endpoint != pending->second.end())
429     {
430         const auto& e = std::get<assocPos>(*endpoint);
431 
432         // Ensure the other side of the association still exists
433         if (interfaceMap.find(std::get<reversePathPos>(e)) ==
434             interfaceMap.end())
435         {
436             endpoint++;
437             continue;
438         }
439 
440         // Add both sides of the association:
441         //  objectPath/forwardType and reversePath/reverseType
442         //
443         // The ownerPath is the reversePath - i.e. the endpoint that
444         // is on D-Bus and owns the org.openbmc.Associations iface.
445         //
446         const auto& ownerPath = std::get<reversePathPos>(e);
447         const auto& owner = std::get<ownerPos>(*endpoint);
448 
449         auto assocPath = objectPath + '/' + std::get<forwardTypePos>(e);
450         auto endpointPath = ownerPath;
451 
452         try
453         {
454             addSingleAssociation(io, server, assocPath, endpointPath, owner,
455                                  ownerPath, assocMaps);
456 
457             // Now the reverse direction (still the same owner and ownerPath)
458             assocPath = endpointPath + '/' + std::get<reverseTypePos>(e);
459             endpointPath = objectPath;
460             addSingleAssociation(io, server, assocPath, endpointPath, owner,
461                                  ownerPath, assocMaps);
462         }
463         catch (const sdbusplus::exception_t& e)
464         {
465             // In some case the interface could not be created on DBus and an
466             // exception is thrown. mapper has no control of the interface/path
467             // of the associations, so it has to catch the error and drop the
468             // association request.
469             std::cerr << "Error adding association: assocPath " << assocPath
470                       << ", endpointPath " << endpointPath
471                       << ", what: " << e.what() << "\n";
472         }
473 
474         // Not pending anymore
475         endpoint = pending->second.erase(endpoint);
476     }
477 
478     if (pending->second.empty())
479     {
480         assocMaps.pending.erase(objectPath);
481     }
482 }
483 
484 void findAssociations(const std::string& endpointPath,
485                       AssociationMaps& assocMaps,
486                       FindAssocResults& associationData)
487 {
488     for (const auto& [sourcePath, owners] : assocMaps.owners)
489     {
490         for (const auto& [owner, assocs] : owners)
491         {
492             for (const auto& [assocPath, endpoints] : assocs)
493             {
494                 if (std::find(endpoints.begin(), endpoints.end(),
495                               endpointPath) != endpoints.end())
496                 {
497                     // assocPath is <path>/<type> which tells us what is on the
498                     // other side of the association.
499                     auto pos = assocPath.rfind('/');
500                     auto otherPath = assocPath.substr(0, pos);
501                     auto otherType = assocPath.substr(pos + 1);
502 
503                     // Now we need to find the endpointPath/<type> ->
504                     // [otherPath] entry so that we can get the type for
505                     // endpointPath's side of the assoc.  Do this by finding
506                     // otherPath as an endpoint, and also checking for
507                     // 'endpointPath/*' as the key.
508                     auto a = std::find_if(
509                         assocs.begin(), assocs.end(),
510                         [&endpointPath, &otherPath](const auto& ap) {
511                         const auto& endpoints = ap.second;
512                         auto endpoint = std::find(endpoints.begin(),
513                                                   endpoints.end(), otherPath);
514                         if (endpoint != endpoints.end())
515                         {
516                             return ap.first.starts_with(endpointPath + '/');
517                         }
518                         return false;
519                     });
520 
521                     if (a != assocs.end())
522                     {
523                         // Pull out the type from endpointPath/<type>
524                         pos = a->first.rfind('/');
525                         auto thisType = a->first.substr(pos + 1);
526 
527                         // Now we know the full association:
528                         // endpointPath/thisType -> otherPath/otherType
529                         Association association{thisType, otherType, otherPath};
530                         associationData.emplace_back(owner, association);
531                     }
532                 }
533             }
534         }
535     }
536 }
537 
538 /** @brief Remove an endpoint for a particular association from D-Bus.
539  *
540  * If the last endpoint is gone, remove the whole association interface,
541  * otherwise just update the D-Bus endpoints property.
542  *
543  * @param[in] assocPath     - the association path
544  * @param[in] endpointPath  - the endpoint path to find and remove
545  * @param[in,out] assocMaps - the association maps
546  * @param[in,out] server    - sdbus system object
547  */
548 void removeAssociationIfacesEntry(boost::asio::io_context& io,
549                                   const std::string& assocPath,
550                                   const std::string& endpointPath,
551                                   AssociationMaps& assocMaps,
552                                   sdbusplus::asio::object_server& server)
553 {
554     auto assoc = assocMaps.ifaces.find(assocPath);
555     if (assoc != assocMaps.ifaces.end())
556     {
557         auto& endpoints = std::get<endpointsPos>(assoc->second);
558         auto e = std::find(endpoints.begin(), endpoints.end(), endpointPath);
559         if (e != endpoints.end())
560         {
561             endpoints.erase(e);
562 
563             scheduleUpdateEndpointsOnDbus(io, server, assocPath, assocMaps);
564         }
565     }
566 }
567 
568 /** @brief Remove an endpoint from the association owners map.
569  *
570  * For a specific association path and owner, remove the endpoint.
571  * Remove all remaining artifacts of that endpoint in the owners map
572  * based on what frees up after the erase.
573  *
574  * @param[in] assocPath     - the association object path
575  * @param[in] endpointPath  - the endpoint object path
576  * @param[in] owner         - the owner of the association
577  * @param[in,out] assocMaps - the association maps
578  */
579 void removeAssociationOwnersEntry(const std::string& assocPath,
580                                   const std::string& endpointPath,
581                                   const std::string& owner,
582                                   AssociationMaps& assocMaps)
583 {
584     auto sources = assocMaps.owners.begin();
585     while (sources != assocMaps.owners.end())
586     {
587         auto owners = sources->second.find(owner);
588         if (owners != sources->second.end())
589         {
590             auto entry = owners->second.find(assocPath);
591             if (entry != owners->second.end())
592             {
593                 auto e = std::find(entry->second.begin(), entry->second.end(),
594                                    endpointPath);
595                 if (e != entry->second.end())
596                 {
597                     entry->second.erase(e);
598                     if (entry->second.empty())
599                     {
600                         owners->second.erase(entry);
601                     }
602                 }
603             }
604 
605             if (owners->second.empty())
606             {
607                 sources->second.erase(owners);
608             }
609         }
610 
611         if (sources->second.empty())
612         {
613             sources = assocMaps.owners.erase(sources);
614             continue;
615         }
616         sources++;
617     }
618 }
619 
620 void moveAssociationToPending(boost::asio::io_context& io,
621                               const std::string& endpointPath,
622                               AssociationMaps& assocMaps,
623                               sdbusplus::asio::object_server& server)
624 {
625     FindAssocResults associationData;
626 
627     // Check which associations this path is an endpoint of, and
628     // then add them to the pending associations map and remove
629     // the associations objects.
630     findAssociations(endpointPath, assocMaps, associationData);
631 
632     for (const auto& [owner, association] : associationData)
633     {
634         const auto& forwardPath = endpointPath;
635         const auto& forwardType = std::get<forwardTypePos>(association);
636         const auto& reversePath = std::get<reversePathPos>(association);
637         const auto& reverseType = std::get<reverseTypePos>(association);
638 
639         addPendingAssociation(forwardPath, forwardType, reversePath,
640                               reverseType, owner, assocMaps);
641 
642         // Remove both sides of the association from assocMaps.ifaces
643         removeAssociationIfacesEntry(io, forwardPath + '/' + forwardType,
644                                      reversePath, assocMaps, server);
645         removeAssociationIfacesEntry(io, reversePath + '/' + reverseType,
646                                      forwardPath, assocMaps, server);
647 
648         // Remove both sides of the association from assocMaps.owners
649         removeAssociationOwnersEntry(forwardPath + '/' + forwardType,
650                                      reversePath, owner, assocMaps);
651         removeAssociationOwnersEntry(reversePath + '/' + reverseType,
652                                      forwardPath, owner, assocMaps);
653     }
654 }
655