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 =
325             std::find_if(endpoints.begin(), endpoints.end(),
326                          [&assoc, &owner](const auto& endpoint) {
327                              return (std::get<ownerPos>(endpoint) == owner) &&
328                                     (std::get<assocPos>(endpoint) == assoc);
329                          });
330         if (e == endpoints.end())
331         {
332             endpoints.emplace_back(owner, std::move(assoc));
333         }
334     }
335 }
336 
337 void removeFromPendingAssociations(const std::string& endpointPath,
338                                    AssociationMaps& assocMaps)
339 {
340     auto assoc = assocMaps.pending.begin();
341     while (assoc != assocMaps.pending.end())
342     {
343         auto endpoint = assoc->second.begin();
344         while (endpoint != assoc->second.end())
345         {
346             auto& e = std::get<assocPos>(*endpoint);
347             if (std::get<reversePathPos>(e) == endpointPath)
348             {
349                 endpoint = assoc->second.erase(endpoint);
350                 continue;
351             }
352 
353             endpoint++;
354         }
355 
356         if (assoc->second.empty())
357         {
358             assoc = assocMaps.pending.erase(assoc);
359             continue;
360         }
361 
362         assoc++;
363     }
364 }
365 
366 void addSingleAssociation(boost::asio::io_context& io,
367                           sdbusplus::asio::object_server& server,
368                           const std::string& assocPath,
369                           const std::string& endpoint, const std::string& owner,
370                           const std::string& ownerPath,
371                           AssociationMaps& assocMaps)
372 {
373     boost::container::flat_set<std::string> endpoints{endpoint};
374 
375     addEndpointsToAssocIfaces(io, server, assocPath, endpoints, assocMaps);
376 
377     AssociationPaths objects;
378     boost::container::flat_set e{endpoint};
379     objects.emplace(assocPath, e);
380 
381     auto a = assocMaps.owners.find(ownerPath);
382     if (a != assocMaps.owners.end())
383     {
384         auto o = a->second.find(owner);
385         if (o != a->second.end())
386         {
387             auto p = o->second.find(assocPath);
388             if (p != o->second.end())
389             {
390                 p->second.emplace(endpoint);
391             }
392             else
393             {
394                 o->second.emplace(assocPath, e);
395             }
396         }
397         else
398         {
399             a->second.emplace(owner, std::move(objects));
400         }
401     }
402     else
403     {
404         boost::container::flat_map<std::string, AssociationPaths> owners;
405         owners.emplace(owner, std::move(objects));
406         assocMaps.owners.emplace(endpoint, owners);
407     }
408 }
409 
410 void checkIfPendingAssociation(boost::asio::io_context& io,
411                                const std::string& objectPath,
412                                const InterfaceMapType& interfaceMap,
413                                AssociationMaps& assocMaps,
414                                sdbusplus::asio::object_server& server)
415 {
416     auto pending = assocMaps.pending.find(objectPath);
417     if (pending == assocMaps.pending.end())
418     {
419         return;
420     }
421 
422     if (interfaceMap.find(objectPath) == interfaceMap.end())
423     {
424         return;
425     }
426 
427     auto endpoint = pending->second.begin();
428 
429     while (endpoint != pending->second.end())
430     {
431         const auto& e = std::get<assocPos>(*endpoint);
432 
433         // Ensure the other side of the association still exists
434         if (interfaceMap.find(std::get<reversePathPos>(e)) ==
435             interfaceMap.end())
436         {
437             endpoint++;
438             continue;
439         }
440 
441         // Add both sides of the association:
442         //  objectPath/forwardType and reversePath/reverseType
443         //
444         // The ownerPath is the reversePath - i.e. the endpoint that
445         // is on D-Bus and owns the org.openbmc.Associations iface.
446         //
447         const auto& ownerPath = std::get<reversePathPos>(e);
448         const auto& owner = std::get<ownerPos>(*endpoint);
449 
450         auto assocPath = objectPath + '/' + std::get<forwardTypePos>(e);
451         auto endpointPath = ownerPath;
452 
453         try
454         {
455             addSingleAssociation(io, server, assocPath, endpointPath, owner,
456                                  ownerPath, assocMaps);
457 
458             // Now the reverse direction (still the same owner and ownerPath)
459             assocPath = endpointPath + '/' + std::get<reverseTypePos>(e);
460             endpointPath = objectPath;
461             addSingleAssociation(io, server, assocPath, endpointPath, owner,
462                                  ownerPath, assocMaps);
463         }
464         catch (const sdbusplus::exception_t& e)
465         {
466             // In some case the interface could not be created on DBus and an
467             // exception is thrown. mapper has no control of the interface/path
468             // of the associations, so it has to catch the error and drop the
469             // association request.
470             std::cerr << "Error adding association: assocPath " << assocPath
471                       << ", endpointPath " << endpointPath
472                       << ", what: " << e.what() << "\n";
473         }
474 
475         // Not pending anymore
476         endpoint = pending->second.erase(endpoint);
477     }
478 
479     if (pending->second.empty())
480     {
481         assocMaps.pending.erase(objectPath);
482     }
483 }
484 
485 void findAssociations(const std::string& endpointPath,
486                       AssociationMaps& assocMaps,
487                       FindAssocResults& associationData)
488 {
489     for (const auto& [sourcePath, owners] : assocMaps.owners)
490     {
491         for (const auto& [owner, assocs] : owners)
492         {
493             for (const auto& [assocPath, endpoints] : assocs)
494             {
495                 if (std::find(endpoints.begin(), endpoints.end(),
496                               endpointPath) != endpoints.end())
497                 {
498                     // assocPath is <path>/<type> which tells us what is on the
499                     // other side of the association.
500                     auto pos = assocPath.rfind('/');
501                     auto otherPath = assocPath.substr(0, pos);
502                     auto otherType = assocPath.substr(pos + 1);
503 
504                     // Now we need to find the endpointPath/<type> ->
505                     // [otherPath] entry so that we can get the type for
506                     // endpointPath's side of the assoc.  Do this by finding
507                     // otherPath as an endpoint, and also checking for
508                     // 'endpointPath/*' as the key.
509                     auto a = std::find_if(
510                         assocs.begin(), assocs.end(),
511                         [&endpointPath, &otherPath](const auto& ap) {
512                             const auto& endpoints = ap.second;
513                             auto endpoint = std::find(
514                                 endpoints.begin(), endpoints.end(), otherPath);
515                             if (endpoint != endpoints.end())
516                             {
517                                 return ap.first.starts_with(endpointPath + '/');
518                             }
519                             return false;
520                         });
521 
522                     if (a != assocs.end())
523                     {
524                         // Pull out the type from endpointPath/<type>
525                         pos = a->first.rfind('/');
526                         auto thisType = a->first.substr(pos + 1);
527 
528                         // Now we know the full association:
529                         // endpointPath/thisType -> otherPath/otherType
530                         Association association{thisType, otherType, otherPath};
531                         associationData.emplace_back(owner, association);
532                     }
533                 }
534             }
535         }
536     }
537 }
538 
539 /** @brief Remove an endpoint for a particular association from D-Bus.
540  *
541  * If the last endpoint is gone, remove the whole association interface,
542  * otherwise just update the D-Bus endpoints property.
543  *
544  * @param[in] assocPath     - the association path
545  * @param[in] endpointPath  - the endpoint path to find and remove
546  * @param[in,out] assocMaps - the association maps
547  * @param[in,out] server    - sdbus system object
548  */
549 void removeAssociationIfacesEntry(boost::asio::io_context& io,
550                                   const std::string& assocPath,
551                                   const std::string& endpointPath,
552                                   AssociationMaps& assocMaps,
553                                   sdbusplus::asio::object_server& server)
554 {
555     auto assoc = assocMaps.ifaces.find(assocPath);
556     if (assoc != assocMaps.ifaces.end())
557     {
558         auto& endpoints = std::get<endpointsPos>(assoc->second);
559         auto e = std::find(endpoints.begin(), endpoints.end(), endpointPath);
560         if (e != endpoints.end())
561         {
562             endpoints.erase(e);
563 
564             scheduleUpdateEndpointsOnDbus(io, server, assocPath, assocMaps);
565         }
566     }
567 }
568 
569 /** @brief Remove an endpoint from the association owners map.
570  *
571  * For a specific association path and owner, remove the endpoint.
572  * Remove all remaining artifacts of that endpoint in the owners map
573  * based on what frees up after the erase.
574  *
575  * @param[in] assocPath     - the association object path
576  * @param[in] endpointPath  - the endpoint object path
577  * @param[in] owner         - the owner of the association
578  * @param[in,out] assocMaps - the association maps
579  */
580 void removeAssociationOwnersEntry(const std::string& assocPath,
581                                   const std::string& endpointPath,
582                                   const std::string& owner,
583                                   AssociationMaps& assocMaps)
584 {
585     auto sources = assocMaps.owners.begin();
586     while (sources != assocMaps.owners.end())
587     {
588         auto owners = sources->second.find(owner);
589         if (owners != sources->second.end())
590         {
591             auto entry = owners->second.find(assocPath);
592             if (entry != owners->second.end())
593             {
594                 auto e = std::find(entry->second.begin(), entry->second.end(),
595                                    endpointPath);
596                 if (e != entry->second.end())
597                 {
598                     entry->second.erase(e);
599                     if (entry->second.empty())
600                     {
601                         owners->second.erase(entry);
602                     }
603                 }
604             }
605 
606             if (owners->second.empty())
607             {
608                 sources->second.erase(owners);
609             }
610         }
611 
612         if (sources->second.empty())
613         {
614             sources = assocMaps.owners.erase(sources);
615             continue;
616         }
617         sources++;
618     }
619 }
620 
621 void moveAssociationToPending(boost::asio::io_context& io,
622                               const std::string& endpointPath,
623                               AssociationMaps& assocMaps,
624                               sdbusplus::asio::object_server& server)
625 {
626     FindAssocResults associationData;
627 
628     // Check which associations this path is an endpoint of, and
629     // then add them to the pending associations map and remove
630     // the associations objects.
631     findAssociations(endpointPath, assocMaps, associationData);
632 
633     for (const auto& [owner, association] : associationData)
634     {
635         const auto& forwardPath = endpointPath;
636         const auto& forwardType = std::get<forwardTypePos>(association);
637         const auto& reversePath = std::get<reversePathPos>(association);
638         const auto& reverseType = std::get<reverseTypePos>(association);
639 
640         addPendingAssociation(forwardPath, forwardType, reversePath,
641                               reverseType, owner, assocMaps);
642 
643         // Remove both sides of the association from assocMaps.ifaces
644         removeAssociationIfacesEntry(io, forwardPath + '/' + forwardType,
645                                      reversePath, assocMaps, server);
646         removeAssociationIfacesEntry(io, reversePath + '/' + reverseType,
647                                      forwardPath, assocMaps, server);
648 
649         // Remove both sides of the association from assocMaps.owners
650         removeAssociationOwnersEntry(forwardPath + '/' + forwardType,
651                                      reversePath, owner, assocMaps);
652         removeAssociationOwnersEntry(reversePath + '/' + reverseType,
653                                      forwardPath, owner, assocMaps);
654     }
655 }
656