Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/lib/cib/cib_ops.c b/lib/cib/cib_ops.c
index 5ba95338b9..0c7d9815b0 100644
--- a/lib/cib/cib_ops.c
+++ b/lib/cib/cib_ops.c
@@ -1,827 +1,813 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <sys/param.h>
#include <sys/types.h>
#include <glib.h>
#include <libxml/tree.h>
#include <libxml/xpath.h> // xmlXPathObject
#include <crm/crm.h>
#include <crm/cib/internal.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
// @TODO: Free this via crm_exit() when libcib gets merged with libcrmcommon
static GHashTable *operation_table = NULL;
static const cib__operation_t cib_ops[] = {
{
PCMK__CIB_REQUEST_ABS_DELETE, cib__op_abs_delete,
cib__op_attr_modifies|cib__op_attr_privileged
},
{
PCMK__CIB_REQUEST_APPLY_PATCH, cib__op_apply_patch,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_BUMP, cib__op_bump,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_COMMIT_TRANSACT, cib__op_commit_transact,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_replaces
|cib__op_attr_writes_through
},
{
PCMK__CIB_REQUEST_CREATE, cib__op_create,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_DELETE, cib__op_delete,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_ERASE, cib__op_erase,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_replaces
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_IS_PRIMARY, cib__op_is_primary,
cib__op_attr_privileged
},
{
PCMK__CIB_REQUEST_MODIFY, cib__op_modify,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_NOOP, cib__op_noop, cib__op_attr_none
},
{
CRM_OP_PING, cib__op_ping, cib__op_attr_none
},
{
// @COMPAT: Drop cib__op_attr_modifies when we drop legacy mode support
PCMK__CIB_REQUEST_PRIMARY, cib__op_primary,
cib__op_attr_modifies|cib__op_attr_privileged|cib__op_attr_local
},
{
PCMK__CIB_REQUEST_QUERY, cib__op_query, cib__op_attr_none
},
{
PCMK__CIB_REQUEST_REPLACE, cib__op_replace,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_replaces
|cib__op_attr_writes_through
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_SECONDARY, cib__op_secondary,
cib__op_attr_privileged|cib__op_attr_local
},
{
PCMK__CIB_REQUEST_SHUTDOWN, cib__op_shutdown, cib__op_attr_privileged
},
{
PCMK__CIB_REQUEST_SYNC_TO_ALL, cib__op_sync_all, cib__op_attr_privileged
},
{
PCMK__CIB_REQUEST_SYNC_TO_ONE, cib__op_sync_one, cib__op_attr_privileged
},
{
PCMK__CIB_REQUEST_UPGRADE, cib__op_upgrade,
cib__op_attr_modifies
|cib__op_attr_privileged
|cib__op_attr_writes_through
|cib__op_attr_transaction
},
{
PCMK__CIB_REQUEST_SCHEMAS, cib__op_schemas, cib__op_attr_local
}
};
/*!
* \internal
* \brief Get the \c cib__operation_t object for a given CIB operation name
*
* \param[in] op CIB operation name
* \param[out] operation Where to store CIB operation object
*
* \return Standard Pacemaker return code
*/
int
cib__get_operation(const char *op, const cib__operation_t **operation)
{
pcmk__assert((op != NULL) && (operation != NULL));
if (operation_table == NULL) {
operation_table = pcmk__strkey_table(NULL, NULL);
for (int lpc = 0; lpc < PCMK__NELEM(cib_ops); lpc++) {
const cib__operation_t *oper = &(cib_ops[lpc]);
g_hash_table_insert(operation_table, (gpointer) oper->name,
(gpointer) oper);
}
}
*operation = g_hash_table_lookup(operation_table, op);
if (*operation == NULL) {
crm_err("Operation %s is invalid", op);
return EINVAL;
}
return pcmk_rc_ok;
}
int
cib_process_query(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
int result = pcmk_ok;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
CRM_CHECK(*answer == NULL, pcmk__xml_free(*answer));
*answer = NULL;
if (pcmk__str_eq(PCMK__XE_ALL, section, pcmk__str_casei)) {
section = NULL;
}
obj_root = pcmk_find_cib_element(existing_cib, section);
if (obj_root == NULL) {
result = -ENXIO;
} else if (options & cib_no_children) {
xmlNode *shallow = pcmk__xe_create(*answer,
(const char *) obj_root->name);
pcmk__xe_copy_attrs(shallow, obj_root, pcmk__xaf_none);
*answer = shallow;
} else {
*answer = obj_root;
}
if (result == pcmk_ok && *answer == NULL) {
crm_err("Error creating query response");
result = -ENOMSG;
}
return result;
}
static int
update_counter(xmlNode *xml_obj, const char *field, bool reset)
{
char *new_value = NULL;
char *old_value = NULL;
int int_value = -1;
if (!reset && crm_element_value(xml_obj, field) != NULL) {
old_value = crm_element_value_copy(xml_obj, field);
}
if (old_value != NULL) {
int_value = atoi(old_value);
new_value = pcmk__itoa(++int_value);
} else {
new_value = pcmk__str_copy("1");
}
crm_trace("Update %s from %s to %s",
field, pcmk__s(old_value, "unset"), new_value);
crm_xml_add(xml_obj, field, new_value);
free(new_value);
free(old_value);
return pcmk_ok;
}
int
cib_process_erase(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
int result = pcmk_ok;
crm_trace("Processing \"%s\" event", op);
if (*result_cib != existing_cib) {
pcmk__xml_free(*result_cib);
}
*result_cib = createEmptyCib(0);
pcmk__xe_copy_attrs(*result_cib, existing_cib, pcmk__xaf_none);
update_counter(*result_cib, PCMK_XA_ADMIN_EPOCH, false);
*answer = NULL;
return result;
}
int
cib_process_upgrade(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer)
{
int rc = 0;
const char *max_schema = crm_element_value(req, PCMK__XA_CIB_SCHEMA_MAX);
const char *original_schema = NULL;
const char *new_schema = NULL;
*answer = NULL;
crm_trace("Processing \"%s\" event with max=%s", op, max_schema);
original_schema = crm_element_value(existing_cib, PCMK_XA_VALIDATE_WITH);
rc = pcmk__update_schema(result_cib, max_schema, true,
!pcmk_is_set(options, cib_verbose));
rc = pcmk_rc2legacy(rc);
new_schema = crm_element_value(*result_cib, PCMK_XA_VALIDATE_WITH);
if (pcmk__cmp_schemas_by_name(new_schema, original_schema) > 0) {
update_counter(*result_cib, PCMK_XA_ADMIN_EPOCH, false);
update_counter(*result_cib, PCMK_XA_EPOCH, true);
update_counter(*result_cib, PCMK_XA_NUM_UPDATES, true);
return pcmk_ok;
}
return rc;
}
int
cib_process_bump(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
int result = pcmk_ok;
crm_trace("Processing %s for epoch='%s'", op,
pcmk__s(crm_element_value(existing_cib, PCMK_XA_EPOCH), ""));
*answer = NULL;
update_counter(*result_cib, PCMK_XA_EPOCH, false);
return result;
}
int
cib_process_replace(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer)
{
int result = pcmk_ok;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
*answer = NULL;
if (input == NULL) {
return -EINVAL;
}
if (pcmk__str_eq(PCMK__XE_ALL, section, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__xe_is(input, section)) {
section = NULL;
}
if (pcmk__xe_is(input, PCMK_XE_CIB)) {
int updates = 0;
int epoch = 0;
int admin_epoch = 0;
int replace_updates = 0;
int replace_epoch = 0;
int replace_admin_epoch = 0;
const char *reason = NULL;
const char *peer = crm_element_value(req, PCMK__XA_SRC);
const char *digest = crm_element_value(req, PCMK__XA_DIGEST);
if (digest) {
char *digest_verify = pcmk__digest_xml(input, true);
if (!pcmk__str_eq(digest_verify, digest, pcmk__str_casei)) {
crm_err("Digest mis-match on replace from %s: %s vs. %s (expected)", peer,
digest_verify, digest);
reason = "digest mismatch";
} else {
crm_info("Digest matched on replace from %s: %s", peer, digest);
}
free(digest_verify);
} else {
crm_trace("No digest to verify");
}
cib_version_details(existing_cib, &admin_epoch, &epoch, &updates);
cib_version_details(input, &replace_admin_epoch, &replace_epoch, &replace_updates);
if (replace_admin_epoch < admin_epoch) {
reason = PCMK_XA_ADMIN_EPOCH;
} else if (replace_admin_epoch > admin_epoch) {
/* no more checks */
} else if (replace_epoch < epoch) {
reason = PCMK_XA_EPOCH;
} else if (replace_epoch > epoch) {
/* no more checks */
} else if (replace_updates < updates) {
reason = PCMK_XA_NUM_UPDATES;
}
if (reason != NULL) {
crm_info("Replacement %d.%d.%d from %s not applied to %d.%d.%d:"
" current %s is greater than the replacement",
replace_admin_epoch, replace_epoch,
replace_updates, peer, admin_epoch, epoch, updates, reason);
result = -pcmk_err_old_data;
} else {
crm_info("Replaced %d.%d.%d with %d.%d.%d from %s",
admin_epoch, epoch, updates,
replace_admin_epoch, replace_epoch, replace_updates, peer);
}
if (*result_cib != existing_cib) {
pcmk__xml_free(*result_cib);
}
*result_cib = pcmk__xml_copy(NULL, input);
} else {
xmlNode *obj_root = NULL;
obj_root = pcmk_find_cib_element(*result_cib, section);
result = pcmk__xe_replace_match(obj_root, input);
result = pcmk_rc2legacy(result);
if (result != pcmk_ok) {
crm_trace("No matching object to replace");
}
}
return result;
}
static int
delete_child(xmlNode *child, void *userdata)
{
xmlNode *obj_root = userdata;
if (pcmk__xe_delete_match(obj_root, child) != pcmk_rc_ok) {
crm_trace("No matching object to delete: %s=%s",
child->name, pcmk__xe_id(child));
}
return pcmk_rc_ok;
}
int
cib_process_delete(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
crm_trace("Processing \"%s\" event", op);
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
obj_root = pcmk_find_cib_element(*result_cib, section);
if (pcmk__xe_is(input, section)) {
pcmk__xe_foreach_child(input, NULL, delete_child, obj_root);
} else {
delete_child(input, obj_root);
}
return pcmk_ok;
}
int
cib_process_modify(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
uint32_t flags = pcmk__xaf_none;
crm_trace("Processing \"%s\" event", op);
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
obj_root = pcmk_find_cib_element(*result_cib, section);
if (obj_root == NULL) {
xmlNode *tmp_section = NULL;
const char *path = pcmk_cib_parent_name_for(section);
if (path == NULL) {
return -EINVAL;
}
tmp_section = pcmk__xe_create(NULL, section);
cib_process_xpath(PCMK__CIB_REQUEST_CREATE, 0, path, NULL, tmp_section,
NULL, result_cib, answer);
pcmk__xml_free(tmp_section);
obj_root = pcmk_find_cib_element(*result_cib, section);
}
CRM_CHECK(obj_root != NULL, return -EINVAL);
if (pcmk_is_set(options, cib_score_update)) {
flags |= pcmk__xaf_score_update;
}
if (pcmk__xe_update_match(obj_root, input, flags) != pcmk_rc_ok) {
if (options & cib_can_create) {
pcmk__xml_copy(obj_root, input);
} else {
return -ENXIO;
}
}
return pcmk_ok;
}
static int
add_cib_object(xmlNode * parent, xmlNode * new_obj)
{
const char *object_name = NULL;
const char *object_id = NULL;
if ((parent == NULL) || (new_obj == NULL)) {
return -EINVAL;
}
object_name = (const char *) new_obj->name;
if (object_name == NULL) {
return -EINVAL;
}
object_id = pcmk__xe_id(new_obj);
if (pcmk__xe_first_child(parent, object_name,
((object_id != NULL)? PCMK_XA_ID : NULL),
object_id)) {
return -EEXIST;
}
if (object_id != NULL) {
crm_trace("Processing creation of <%s " PCMK_XA_ID "='%s'>",
object_name, object_id);
} else {
crm_trace("Processing creation of <%s>", object_name);
}
/* @COMPAT PCMK__XA_REPLACE is deprecated since 2.1.6. Due to a legacy use
* case, PCMK__XA_REPLACE has special meaning and should not be included in
* the newly created object until we can break behavioral backward
* compatibility.
*
* At a compatibility break, drop this and drop the definition of
* PCMK__XA_REPLACE. Treat it like any other attribute.
*/
pcmk__xml_tree_foreach(new_obj, pcmk__xe_remove_attr_cb,
(void *) PCMK__XA_REPLACE);
pcmk__xml_copy(parent, new_obj);
return pcmk_ok;
}
static bool
update_results(xmlNode *failed, xmlNode *target, const char *operation,
int return_code)
{
xmlNode *xml_node = NULL;
bool was_error = false;
const char *error_msg = NULL;
if (return_code != pcmk_ok) {
error_msg = pcmk_strerror(return_code);
was_error = true;
xml_node = pcmk__xe_create(failed, PCMK__XE_FAILED_UPDATE);
pcmk__xml_copy(xml_node, target);
crm_xml_add(xml_node, PCMK_XA_ID, pcmk__xe_id(target));
crm_xml_add(xml_node, PCMK_XA_OBJECT_TYPE, (const char *) target->name);
crm_xml_add(xml_node, PCMK_XA_OPERATION, operation);
crm_xml_add(xml_node, PCMK_XA_REASON, error_msg);
crm_warn("Action %s failed: %s (cde=%d)",
operation, error_msg, return_code);
}
return was_error;
}
int
cib_process_create(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *failed = NULL;
int result = pcmk_ok;
xmlNode *update_section = NULL;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (pcmk__str_eq(PCMK__XE_ALL, section, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__str_eq(section, PCMK_XE_CIB, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__xe_is(input, PCMK_XE_CIB)) {
section = NULL;
}
CRM_CHECK(strcmp(op, PCMK__CIB_REQUEST_CREATE) == 0, return -EINVAL);
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
if (section == NULL) {
return cib_process_modify(op, options, section, req, input, existing_cib, result_cib,
answer);
}
// @COMPAT Deprecated since 2.1.8
failed = pcmk__xe_create(NULL, PCMK__XE_FAILED);
update_section = pcmk_find_cib_element(*result_cib, section);
if (pcmk__xe_is(input, section)) {
xmlNode *a_child = NULL;
for (a_child = pcmk__xml_first_child(input); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
result = add_cib_object(update_section, a_child);
if (update_results(failed, a_child, op, result)) {
break;
}
}
} else {
result = add_cib_object(update_section, input);
update_results(failed, input, op, result);
}
if ((result == pcmk_ok) && (failed->children != NULL)) {
result = -EINVAL;
}
if (result != pcmk_ok) {
crm_log_xml_err(failed, "CIB Update failures");
*answer = failed;
} else {
pcmk__xml_free(failed);
}
return result;
}
int
cib_process_diff(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
const char *originator = NULL;
if (req != NULL) {
originator = crm_element_value(req, PCMK__XA_SRC);
}
crm_trace("Processing \"%s\" event from %s%s",
op, originator,
(pcmk_is_set(options, cib_force_diff)? " (global update)" : ""));
if (*result_cib != existing_cib) {
pcmk__xml_free(*result_cib);
}
*result_cib = pcmk__xml_copy(NULL, existing_cib);
return xml_apply_patchset(*result_cib, input, TRUE);
}
int
cib_process_xpath(const char *op, int options, const char *section,
const xmlNode *req, xmlNode *input, xmlNode *existing_cib,
xmlNode **result_cib, xmlNode **answer)
{
int lpc = 0;
int max = 0;
int rc = pcmk_ok;
bool is_query = pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none);
xmlXPathObject *xpathObj = NULL;
crm_trace("Processing \"%s\" event", op);
if (is_query) {
xpathObj = pcmk__xpath_search(existing_cib->doc, section);
} else {
xpathObj = pcmk__xpath_search((*result_cib)->doc, section);
}
max = pcmk__xpath_num_results(xpathObj);
if ((max < 1)
&& pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) {
crm_debug("%s was already removed", section);
} else if (max < 1) {
crm_debug("%s: %s does not exist", op, section);
rc = -ENXIO;
} else if (is_query) {
if (max > 1) {
*answer = pcmk__xe_create(NULL, PCMK__XE_XPATH_QUERY);
}
}
if (pcmk_is_set(options, cib_multiple)
&& pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) {
dedupXpathResults(xpathObj);
}
for (lpc = 0; lpc < max; lpc++) {
xmlChar *path = NULL;
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
- /* @TODO This is a longstanding bug in the CIB API. If an XPath
- * expression matches a non-element node (for example, an attribute),
- * then we operate on the parent element. Similarly, if it matches the
- * document itself, then we operate on the root element (which at least
- * seems more reasonable).
- *
- * Some CIB operations might not make much sense for non-element nodes.
- * However, operating on a matched node's parent is surprising behavior.
- */
- match = pcmk__xpath_match_element(match);
- if (match == NULL) {
- continue;
- }
-
path = xmlGetNodePath(match);
crm_debug("Processing %s op for %s with %s", op, section, path);
free(path);
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) {
if (match == *result_cib) {
/* Attempting to delete the whole "/cib" */
crm_warn("Cannot perform %s for %s: The xpath is addressing the whole /cib", op, section);
rc = -EINVAL;
break;
}
pcmk__xml_free(match);
if ((options & cib_multiple) == 0) {
break;
}
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_MODIFY, pcmk__str_none)) {
uint32_t flags = pcmk__xaf_none;
if (pcmk_is_set(options, cib_score_update)) {
flags |= pcmk__xaf_score_update;
}
if (pcmk__xe_update_match(match, input, flags) != pcmk_rc_ok) {
rc = -ENXIO;
} else if ((options & cib_multiple) == 0) {
break;
}
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_CREATE, pcmk__str_none)) {
pcmk__xml_copy(match, input);
break;
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none)) {
if (options & cib_no_children) {
xmlNode *shallow = pcmk__xe_create(*answer,
(const char *) match->name);
pcmk__xe_copy_attrs(shallow, match, pcmk__xaf_none);
if (*answer == NULL) {
*answer = shallow;
}
} else if (options & cib_xpath_address) {
char *path = NULL;
xmlNode *parent = match;
while (parent && parent->type == XML_ELEMENT_NODE) {
const char *id = crm_element_value(parent, PCMK_XA_ID);
char *new_path = NULL;
if (id) {
new_path = crm_strdup_printf("/%s[@" PCMK_XA_ID "='%s']"
"%s",
parent->name, id,
pcmk__s(path, ""));
} else {
new_path = crm_strdup_printf("/%s%s", parent->name,
pcmk__s(path, ""));
}
free(path);
path = new_path;
parent = parent->parent;
}
crm_trace("Got: %s", path);
if (*answer == NULL) {
*answer = pcmk__xe_create(NULL, PCMK__XE_XPATH_QUERY);
}
parent = pcmk__xe_create(*answer, PCMK__XE_XPATH_QUERY_PATH);
crm_xml_add(parent, PCMK_XA_ID, path);
free(path);
} else if (*answer) {
pcmk__xml_copy(*answer, match);
} else {
*answer = match;
}
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE,
pcmk__str_none)) {
xmlNode *parent = match->parent;
pcmk__xml_free(match);
pcmk__xml_copy(parent, input);
if ((options & cib_multiple) == 0) {
break;
}
}
}
freeXpathObject(xpathObj);
return rc;
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 10, 1:12 AM (12 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2009444
Default Alt Text
(25 KB)

Event Timeline