1    	/*
2    	 * Copyright 2022-2026 the Pacemaker project contributors
3    	 *
4    	 * The version control history for this file may have further details.
5    	 *
6    	 * This source code is licensed under the GNU General Public License version 2
7    	 * or later (GPLv2+) WITHOUT ANY WARRANTY.
8    	 */
9    	
10   	#include <crm_internal.h>
11   	
12   	#include <stdbool.h>
13   	
14   	#include <crm/common/xml.h>
15   	
16   	#include "pacemaker-attrd.h"
17   	
18   	/* A hash table storing clients that are waiting on a sync point to be reached.
19   	 * The key is waitlist_client - just a plain int.  The obvious key would be
20   	 * the IPC client's ID, but this is not guaranteed to be unique.  A single client
21   	 * could be waiting on a sync point for multiple attributes at the same time.
22   	 *
23   	 * It is not expected that this hash table will ever be especially large.
24   	 */
25   	static GHashTable *waitlist = NULL;
26   	static int waitlist_client = 0;
27   	
28   	struct waitlist_node {
29   	    /* What kind of sync point does this node describe? */
30   	    enum attrd_sync_point sync_point;
31   	
32   	    /* Information required to construct and send a reply to the client. */
33   	    char *client_id;
34   	    uint32_t ipc_id;
35   	    uint32_t flags;
36   	};
37   	
38   	/* A hash table storing information on in-progress IPC requests that are awaiting
39   	 * confirmations.  These requests are currently being processed by peer attrds and
40   	 * we are waiting to receive confirmation messages from each peer indicating that
41   	 * processing is complete.
42   	 *
43   	 * Multiple requests could be waiting on confirmations at the same time.
44   	 *
45   	 * The key is the unique callid for the IPC request, and the value is a
46   	 * confirmation_action struct.
47   	 */
48   	static GHashTable *expected_confirmations = NULL;
49   	
50   	/*!
51   	 * \internal
52   	 * \brief A structure describing a single IPC request that is awaiting confirmations
53   	 */
54   	struct confirmation_action {
55   	    /*!
56   	     * \brief A list of peer attrds that we are waiting to receive confirmation
57   	     *        messages from
58   	     *
59   	     * This list is dynamic - as confirmations arrive from peer attrds, they will
60   	     * be removed from this list.  When the list is empty, all peers have processed
61   	     * the request and the associated confirmation action will be taken.
62   	     */
63   	    GList *respondents;
64   	
65   	    /*!
66   	     * \brief A timer that will be used to remove the client should it time out
67   	     *        before receiving all confirmations
68   	     */
69   	    mainloop_timer_t *timer;
70   	
71   	    /*!
72   	     * \brief A function to run when all confirmations have been received
73   	     */
74   	    attrd_confirmation_action_fn fn;
75   	
76   	    /*!
77   	     * \brief Information required to construct and send a reply to the client
78   	     */
79   	    char *client_id;
80   	    uint32_t ipc_id;
81   	    uint32_t flags;
82   	
83   	    /*!
84   	     * \brief The XML request containing the callid associated with this action
85   	     */
86   	    void *xml;
87   	};
88   	
89   	static void
90   	next_key(void)
91   	{
92   	    do {
93   	        waitlist_client++;
94   	        if (waitlist_client < 0) {
95   	            waitlist_client = 1;
96   	        }
97   	    } while (g_hash_table_contains(waitlist, GINT_TO_POINTER(waitlist_client)));
98   	}
99   	
100  	static void
101  	free_waitlist_node(gpointer data)
102  	{
103  	    struct waitlist_node *wl = (struct waitlist_node *) data;
104  	
105  	    free(wl->client_id);
106  	    free(wl);
107  	}
108  	
109  	static const char *
110  	sync_point_str(enum attrd_sync_point sync_point)
111  	{
112  	    if (sync_point == attrd_sync_point_local) {
113  	        return PCMK__VALUE_LOCAL;
114  	    } else if  (sync_point == attrd_sync_point_cluster) {
115  	        return PCMK__VALUE_CLUSTER;
116  	    } else {
117  	        return PCMK_VALUE_UNKNOWN;
118  	    }
119  	}
120  	
121  	/*!
122  	 * \internal
123  	 * \brief Add a client to the attrd waitlist
124  	 *
125  	 * Typically, a client receives an ACK for its XML IPC request immediately.  However,
126  	 * some clients want to wait until their request has been processed and taken effect.
127  	 * This is called a sync point.  Any client placed on this waitlist will have its
128  	 * ACK message delayed until either its requested sync point is hit, or until it
129  	 * times out.
130  	 *
131  	 * The XML IPC request must specify the type of sync point it wants to wait for.
132  	 *
133  	 * \param[in,out] request   The request describing the client to place on the waitlist.
134  	 */
135  	void
136  	attrd_add_client_to_waitlist(pcmk__request_t *request)
137  	{
138  	    const char *sync_point = attrd_request_sync_point(request->xml);
139  	    struct waitlist_node *wl = NULL;
140  	
141  	    if (sync_point == NULL) {
142  	        return;
143  	    }
144  	
145  	    if (waitlist == NULL) {
146  	        waitlist = pcmk__intkey_table(free_waitlist_node);
147  	    }
148  	
149  	    wl = pcmk__assert_alloc(1, sizeof(struct waitlist_node));
150  	
151  	    if (pcmk__str_eq(sync_point, PCMK__VALUE_LOCAL, pcmk__str_none)) {
152  	        wl->sync_point = attrd_sync_point_local;
153  	    } else if (pcmk__str_eq(sync_point, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
154  	        wl->sync_point = attrd_sync_point_cluster;
155  	    } else {
156  	        free_waitlist_node(wl);
157  	        return;
158  	    }
159  	
160  	    wl->client_id = pcmk__str_copy(request->ipc_client->id);
161  	    wl->ipc_id = request->ipc_id;
162  	    wl->flags = request->flags;
163  	
164  	    next_key();
165  	    pcmk__intkey_table_insert(waitlist, waitlist_client, wl);
166  	
167  	    pcmk__trace("Added client %s to waitlist for %s sync point",
168  	                wl->client_id, sync_point_str(wl->sync_point));
169  	    pcmk__trace("%u clients now on waitlist", g_hash_table_size(waitlist));
170  	
171  	    /* And then add the key to the request XML so we can uniquely identify
172  	     * it when it comes time to issue the ACK.
173  	     */
174  	    pcmk__xe_set_int(request->xml, PCMK__XA_CALL_ID, waitlist_client);
175  	}
176  	
177  	/*!
178  	 * \internal
179  	 * \brief Free all memory associated with the waitlist.  This is most typically
180  	 *        used when attrd shuts down.
181  	 */
182  	void
183  	attrd_free_waitlist(void)
184  	{
185  	    g_clear_pointer(&waitlist, g_hash_table_destroy);
186  	}
187  	
188  	/*!
189  	 * \internal
190  	 * \brief Unconditionally remove a client from the waitlist, such as when the client
191  	 *        node disconnects from the cluster
192  	 *
193  	 * \param[in] client    The client to remove
194  	 */
195  	void
196  	attrd_remove_client_from_waitlist(pcmk__client_t *client)
197  	{
198  	    GHashTableIter iter;
199  	    gpointer value;
200  	
201  	    if (waitlist == NULL) {
202  	        return;
203  	    }
204  	
205  	    g_hash_table_iter_init(&iter, waitlist);
206  	
207  	    while (g_hash_table_iter_next(&iter, NULL, &value)) {
208  	        struct waitlist_node *wl = (struct waitlist_node *) value;
209  	
210  	        if (pcmk__str_eq(wl->client_id, client->id, pcmk__str_none)) {
211  	            g_hash_table_iter_remove(&iter);
212  	            pcmk__trace("%u clients now on waitlist",
213  	                        g_hash_table_size(waitlist));
214  	        }
215  	    }
216  	}
217  	
218  	/*!
219  	 * \internal
220  	 * \brief Send an IPC ACK message to all awaiting clients
221  	 *
222  	 * This function will search the waitlist for all clients that are currently awaiting
223  	 * an ACK indicating their attrd operation is complete.  Only those clients with a
224  	 * matching sync point type and callid from their original XML IPC request will be
225  	 * ACKed.  Once they have received an ACK, they will be removed from the waitlist.
226  	 *
227  	 * \param[in] sync_point What kind of sync point have we hit?
228  	 * \param[in] xml        The original XML IPC request.
229  	 */
230  	void
231  	attrd_ack_waitlist_clients(enum attrd_sync_point sync_point, const xmlNode *xml)
232  	{
233  	    int callid;
234  	    gpointer value;
235  	
236  	    if (waitlist == NULL) {
237  	        return;
238  	    }
239  	
240  	    if (pcmk__xe_get_int(xml, PCMK__XA_CALL_ID, &callid) != pcmk_rc_ok) {
241  	        pcmk__warn("Could not get callid from request XML");
242  	        return;
243  	    }
244  	
245  	    value = pcmk__intkey_table_lookup(waitlist, callid);
246  	    if (value != NULL) {
247  	        struct waitlist_node *wl = (struct waitlist_node *) value;
248  	        pcmk__client_t *client = NULL;
249  	
250  	        if (wl->sync_point != sync_point) {
251  	            return;
252  	        }
253  	
254  	        pcmk__notice("Alerting client %s for reached %s sync point",
255  	                     wl->client_id, sync_point_str(wl->sync_point));
256  	
257  	        client = pcmk__find_client_by_id(wl->client_id);
258  	        if (client == NULL) {
259  	            return;
260  	        }
261  	
262  	        attrd_send_ack(client, wl->ipc_id, wl->flags | crm_ipc_client_response);
263  	
264  	        /* And then remove the client so it doesn't get alerted again. */
265  	        pcmk__intkey_table_remove(waitlist, callid);
266  	
267  	        pcmk__trace("%u clients now on waitlist", g_hash_table_size(waitlist));
268  	    }
269  	}
270  	
271  	/*!
272  	 * \internal
273  	 * \brief Action to take when a cluster sync point is hit for a
274  	 *        PCMK__ATTRD_CMD_UPDATE* message.
275  	 *
276  	 * \param[in] xml  The request that should be passed along to
277  	 *                 attrd_ack_waitlist_clients.  This should be the original
278  	 *                 IPC request containing the callid for this update message.
279  	 */
280  	int
281  	attrd_cluster_sync_point_update(xmlNode *xml)
282  	{
283  	    pcmk__trace("Hit cluster sync point for attribute update");
284  	    attrd_ack_waitlist_clients(attrd_sync_point_cluster, xml);
285  	    return pcmk_rc_ok;
286  	}
287  	
288  	/*!
289  	 * \internal
290  	 * \brief Return the sync point attribute for an IPC request
291  	 *
292  	 * This function will check both the top-level element of \p xml for a sync
293  	 * point attribute, as well as all of its \p op children, if any.  The latter
294  	 * is useful for newer versions of attrd that can put multiple IPC requests
295  	 * into a single message.
296  	 *
297  	 * \param[in] xml   An XML IPC request
298  	 *
299  	 * \note It is assumed that if one child element has a sync point attribute,
300  	 *       all will have a sync point attribute and they will all be the same
301  	 *       sync point.  No other configuration is supported.
302  	 *
303  	 * \return The sync point attribute of \p xml, or NULL if none.
304  	 */
305  	const char *
306  	attrd_request_sync_point(xmlNode *xml)
307  	{
308  	    CRM_CHECK(xml != NULL, return NULL);
309  	
310  	    if (xml->children != NULL) {
311  	        xmlNode *child = pcmk__xe_first_child(xml, PCMK_XE_OP,
312  	                                              PCMK__XA_ATTR_SYNC_POINT, NULL);
313  	
314  	        if (child) {
315  	            return pcmk__xe_get(child, PCMK__XA_ATTR_SYNC_POINT);
316  	        } else {
317  	            return NULL;
318  	        }
319  	
320  	    } else {
321  	        return pcmk__xe_get(xml, PCMK__XA_ATTR_SYNC_POINT);
322  	    }
323  	}
324  	
325  	/*!
326  	 * \internal
327  	 * \brief Does an IPC request contain any sync point attribute?
328  	 *
329  	 * \param[in] xml   An XML IPC request
330  	 *
331  	 * \return true if there's a sync point attribute, false otherwise
332  	 */
333  	bool
334  	attrd_request_has_sync_point(xmlNode *xml)
335  	{
336  	    return attrd_request_sync_point(xml) != NULL;
337  	}
338  	
339  	static void
340  	free_action(gpointer data)
341  	{
342  	    struct confirmation_action *action = (struct confirmation_action *) data;
343  	    g_list_free_full(action->respondents, free);
344  	    mainloop_timer_del(action->timer);
345  	    pcmk__xml_free(action->xml);
346  	    free(action->client_id);
347  	    free(action);
348  	}
349  	
350  	/* Remove an IPC request from the expected_confirmations table if the peer attrds
351  	 * don't respond before the timeout is hit.  We set the timeout to 15s.  The exact
352  	 * number isn't critical - we just want to make sure that the table eventually gets
353  	 * cleared of things that didn't complete.
354  	 */
355  	static gboolean
356  	confirmation_timeout_cb(gpointer data)
357  	{
358  	    struct confirmation_action *action = (struct confirmation_action *) data;
359  	
360  	    GHashTableIter iter;
361  	    gpointer value;
362  	
363  	    if (expected_confirmations == NULL) {
364  	        return G_SOURCE_REMOVE;
365  	    }
366  	
367  	    g_hash_table_iter_init(&iter, expected_confirmations);
368  	
369  	    while (g_hash_table_iter_next(&iter, NULL, &value)) {
370  	        if (value == action) {
371  	            pcmk__client_t *client = pcmk__find_client_by_id(action->client_id);
372  	            if (client == NULL) {
373  	                return G_SOURCE_REMOVE;
374  	            }
375  	
376  	            pcmk__trace("Timed out waiting for confirmations for client %s",
377  	                        client->id);
378  	            pcmk__ipc_send_ack(client, action->ipc_id,
379  	                               action->flags|crm_ipc_client_response,
380  	                               ATTRD_PROTOCOL_VERSION, CRM_EX_TIMEOUT);
381  	
382  	            g_hash_table_iter_remove(&iter);
383  	            pcmk__trace("%u requests now in expected confirmations table",
384  	                        g_hash_table_size(expected_confirmations));
385  	            break;
386  	        }
387  	    }
388  	
389  	    return G_SOURCE_REMOVE;
390  	}
391  	
392  	/*!
393  	 * \internal
394  	 * \brief When a peer disconnects from the cluster, no longer wait for its confirmation
395  	 *        for any IPC action.  If this peer is the last one being waited on, this will
396  	 *        trigger the confirmation action.
397  	 *
398  	 * \param[in] host   The disconnecting peer attrd's uname
399  	 */
400  	void
401  	attrd_do_not_expect_from_peer(const char *host)
402  	{
403  	    GList *keys = NULL;
404  	
405  	    if (expected_confirmations == NULL) {
406  	        return;
407  	    }
408  	
409  	    keys = g_hash_table_get_keys(expected_confirmations);
410  	
411  	    pcmk__trace("Removing peer %s from expected confirmations", host);
412  	
413  	    for (GList *node = keys; node != NULL; node = node->next) {
414  	        int callid = *(int *) node->data;
415  	        attrd_handle_confirmation(callid, host);
416  	    }
417  	
418  	    g_list_free(keys);
419  	}
420  	
421  	/*!
422  	 * \internal
423  	 * \brief When a client disconnects from the cluster, no longer wait on confirmations
424  	 *        for it.  Because the peer attrds may still be processing the original IPC
425  	 *        message, they may still send us confirmations.  However, we will take no
426  	 *        action on them.
427  	 *
428  	 * \param[in] client    The disconnecting client
429  	 */
430  	void
431  	attrd_do_not_wait_for_client(pcmk__client_t *client)
432  	{
433  	    GHashTableIter iter;
434  	    gpointer value;
435  	
436  	    if (expected_confirmations == NULL) {
437  	        return;
438  	    }
439  	
440  	    g_hash_table_iter_init(&iter, expected_confirmations);
441  	
442  	    while (g_hash_table_iter_next(&iter, NULL, &value)) {
443  	        struct confirmation_action *action = (struct confirmation_action *) value;
444  	
445  	        if (pcmk__str_eq(action->client_id, client->id, pcmk__str_none)) {
446  	            pcmk__trace("Removing client %s from expected confirmations",
447  	                        client->id);
448  	            g_hash_table_iter_remove(&iter);
449  	            pcmk__trace("%u requests now in expected confirmations table",
450  	                        g_hash_table_size(expected_confirmations));
451  	            break;
452  	        }
453  	    }
454  	}
455  	
456  	/*!
457  	 * \internal
458  	 * \brief Register some action to be taken when IPC request confirmations are
459  	 *        received
460  	 *
461  	 * When this function is called, a list of all peer attrds that support confirming
462  	 * requests is generated.  As confirmations from these peer attrds are received,
463  	 * they are removed from this list.  When the list is empty, the registered action
464  	 * will be called.
465  	 *
466  	 * \note This function should always be called before attrd_send_message is called
467  	 *       to broadcast to the peers to ensure that we know what replies we are
468  	 *       waiting on.  Otherwise, it is possible the peer could finish and confirm
469  	 *       before we know to expect it.
470  	 *
471  	 * \param[in] request The request that is awaiting confirmations
472  	 * \param[in] fn      A function to be run after all confirmations are received
473  	 */
474  	void
475  	attrd_expect_confirmations(pcmk__request_t *request, attrd_confirmation_action_fn fn)
476  	{
477  	    struct confirmation_action *action = NULL;
478  	    GHashTableIter iter;
479  	    gpointer host, ver;
480  	    GList *respondents = NULL;
481  	    int callid;
482  	
483  	    if (expected_confirmations == NULL) {
484  	        expected_confirmations = pcmk__intkey_table((GDestroyNotify) free_action);
485  	    }
486  	
487  	    if (pcmk__xe_get_int(request->xml, PCMK__XA_CALL_ID,
488  	                         &callid) != pcmk_rc_ok) {
489  	        pcmk__err("Could not get callid from xml");
490  	        return;
491  	    }
492  	
493  	    if (pcmk__intkey_table_lookup(expected_confirmations, callid)) {
494  	        pcmk__err("Already waiting on confirmations for call id %d", callid);
495  	        return;
496  	    }
497  	
498  	    g_hash_table_iter_init(&iter, peer_protocol_vers);
499  	    while (g_hash_table_iter_next(&iter, &host, &ver)) {
500  	        if (ATTRD_SUPPORTS_CONFIRMATION(GPOINTER_TO_INT(ver))) {
501  	            respondents = g_list_prepend(respondents,
502  	                                         pcmk__str_copy((char *) host));
503  	        }
504  	    }
505  	
506  	    action = pcmk__assert_alloc(1, sizeof(struct confirmation_action));
507  	
508  	    action->respondents = respondents;
509  	    action->fn = fn;
510  	    action->xml = pcmk__xml_copy(NULL, request->xml);
511  	    action->client_id = pcmk__str_copy(request->ipc_client->id);
512  	    action->ipc_id = request->ipc_id;
513  	    action->flags = request->flags;
514  	
515  	    action->timer = mainloop_timer_add(NULL, 15000, FALSE, confirmation_timeout_cb, action);
516  	    mainloop_timer_start(action->timer);
517  	
518  	    pcmk__intkey_table_insert(expected_confirmations, callid, action);
519  	    pcmk__trace("Callid %d now waiting on %u confirmations", callid,
520  	                g_list_length(respondents));
521  	    pcmk__trace("%u requests now in expected confirmations table",
522  	                g_hash_table_size(expected_confirmations));
523  	}
524  	
525  	void
526  	attrd_free_confirmations(void)
527  	{
CID (unavailable; MK=ea53f96c13cab808ee1b8783848fc084) (#1 of 1): Inconsistent C union access (INCONSISTENT_UNION_ACCESS):
(1) Event assign_union_field: The union field "in" of "_pp" is written.
(2) Event inconsistent_union_field_access: In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in".
528  	    g_clear_pointer(&expected_confirmations, g_hash_table_destroy);
529  	}
530  	
531  	/*!
532  	 * \internal
533  	 * \brief Process a confirmation message from a peer attrd
534  	 *
535  	 * This function is called every time a PCMK__ATTRD_CMD_CONFIRM message is
536  	 * received from a peer attrd.  If this is the last confirmation we are waiting
537  	 * on for a given operation, the registered action will be called.
538  	 *
539  	 * \param[in] callid The unique callid for the XML IPC request
540  	 * \param[in] host   The confirming peer attrd's uname
541  	 */
542  	void
543  	attrd_handle_confirmation(int callid, const char *host)
544  	{
545  	    struct confirmation_action *action = NULL;
546  	    GList *node = NULL;
547  	
548  	    if (expected_confirmations == NULL) {
549  	        return;
550  	    }
551  	
552  	    action = pcmk__intkey_table_lookup(expected_confirmations, callid);
553  	    if (action == NULL) {
554  	        return;
555  	    }
556  	
557  	    node = g_list_find_custom(action->respondents, host, (GCompareFunc) strcasecmp);
558  	
559  	    if (node == NULL) {
560  	        return;
561  	    }
562  	
563  	    action->respondents = g_list_remove(action->respondents, node->data);
564  	    pcmk__trace("Callid %d now waiting on %u confirmations", callid,
565  	                g_list_length(action->respondents));
566  	
567  	    if (action->respondents == NULL) {
568  	        action->fn(action->xml);
569  	        pcmk__intkey_table_remove(expected_confirmations, callid);
570  	        pcmk__trace("%u requests now in expected confirmations table",
571  	                    g_hash_table_size(expected_confirmations));
572  	    }
573  	}
574