Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/daemons/schedulerd/pacemaker-schedulerd.c b/daemons/schedulerd/pacemaker-schedulerd.c
index 30a7bf976d..1fe37923b6 100644
--- a/daemons/schedulerd/pacemaker-schedulerd.c
+++ b/daemons/schedulerd/pacemaker-schedulerd.c
@@ -1,183 +1,186 @@
/*
* Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/crm.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <libxml/parser.h>
#include <crm/common/ipcs.h>
#include <crm/common/mainloop.h>
#include <crm/pengine/internal.h>
+#include <sched_allocate.h>
#include <crm/msg_xml.h>
#define OPTARGS "hVc"
static GMainLoop *mainloop = NULL;
static qb_ipcs_service_t *ipcs = NULL;
void pengine_shutdown(int nsig);
static int32_t
pe_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid)
{
crm_trace("Connection %p", c);
if (crm_client_new(c, uid, gid) == NULL) {
return -EIO;
}
return 0;
}
static void
pe_ipc_created(qb_ipcs_connection_t * c)
{
crm_trace("Connection %p", c);
}
gboolean process_pe_message(xmlNode * msg, xmlNode * xml_data, crm_client_t * sender);
static int32_t
pe_ipc_dispatch(qb_ipcs_connection_t * qbc, void *data, size_t size)
{
uint32_t id = 0;
uint32_t flags = 0;
crm_client_t *c = crm_client_get(qbc);
xmlNode *msg = crm_ipcs_recv(c, data, size, &id, &flags);
crm_ipcs_send_ack(c, id, flags, "ack", __FUNCTION__, __LINE__);
if (msg != NULL) {
xmlNode *data_xml = get_message_xml(msg, F_CRM_DATA);
process_pe_message(msg, data_xml, c);
free_xml(msg);
}
return 0;
}
/* Error code means? */
static int32_t
pe_ipc_closed(qb_ipcs_connection_t * c)
{
crm_client_t *client = crm_client_get(c);
if (client == NULL) {
return 0;
}
crm_trace("Connection %p", c);
crm_client_destroy(client);
return 0;
}
static void
pe_ipc_destroy(qb_ipcs_connection_t * c)
{
crm_trace("Connection %p", c);
pe_ipc_closed(c);
}
struct qb_ipcs_service_handlers ipc_callbacks = {
.connection_accept = pe_ipc_accept,
.connection_created = pe_ipc_created,
.msg_process = pe_ipc_dispatch,
.connection_closed = pe_ipc_closed,
.connection_destroyed = pe_ipc_destroy
};
/* *INDENT-OFF* */
static struct crm_option long_options[] = {
/* Top-level Options */
{"help", 0, 0, '?', "\tThis text"},
{"verbose", 0, 0, 'V', "\tIncrease debug output"},
{0, 0, 0, 0}
};
/* *INDENT-ON* */
int
main(int argc, char **argv)
{
int flag;
int index = 0;
int argerr = 0;
crm_log_preinit(NULL, argc, argv);
crm_set_options(NULL, "[options]",
long_options, "Daemon for calculating the cluster's response to events");
mainloop_add_signal(SIGTERM, pengine_shutdown);
while (1) {
flag = crm_get_option(argc, argv, &index);
if (flag == -1)
break;
switch (flag) {
case 'V':
crm_bump_log_level(argc, argv);
break;
case 'h': /* Help message */
crm_help('?', CRM_EX_OK);
break;
default:
++argerr;
break;
}
}
if (argc - optind == 1 && safe_str_eq("metadata", argv[optind])) {
pe_metadata();
return CRM_EX_OK;
}
if (optind > argc) {
++argerr;
}
if (argerr) {
crm_help('?', CRM_EX_USAGE);
}
crm_log_init(NULL, LOG_INFO, TRUE, FALSE, argc, argv, FALSE);
if (pcmk__daemon_can_write(PE_STATE_DIR, NULL) == FALSE) {
crm_err("Terminating due to bad permissions on " PE_STATE_DIR);
fprintf(stderr,
"ERROR: Bad permissions on " PE_STATE_DIR " (see logs for details)\n");
fflush(stderr);
return CRM_EX_FATAL;
}
crm_debug("Init server comms");
ipcs = mainloop_add_ipc_server(CRM_SYSTEM_PENGINE, QB_IPC_SHM, &ipc_callbacks);
if (ipcs == NULL) {
crm_err("Failed to create IPC server: shutting down and inhibiting respawn");
crm_exit(CRM_EX_FATAL);
}
/* Create the mainloop and run it... */
crm_info("Starting %s", crm_system_name);
mainloop = g_main_loop_new(NULL, FALSE);
g_main_loop_run(mainloop);
+ libpengine_fini();
crm_info("Exiting %s", crm_system_name);
return crm_exit(CRM_EX_OK);
}
void
pengine_shutdown(int nsig)
{
mainloop_del_ipc_server(ipcs);
+ libpengine_fini();
crm_exit(CRM_EX_OK);
}
diff --git a/daemons/schedulerd/sched_allocate.h b/daemons/schedulerd/sched_allocate.h
index 42cb820b9d..aa44e2c4c9 100644
--- a/daemons/schedulerd/sched_allocate.h
+++ b/daemons/schedulerd/sched_allocate.h
@@ -1,156 +1,157 @@
/*
* Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef SCHED_ALLOCATE__H
# define SCHED_ALLOCATE__H
# include <glib.h>
# include <crm/common/xml.h>
# include <crm/pengine/status.h>
# include <crm/pengine/complex.h>
# include <crm/pengine/internal.h>
# include <pacemaker-schedulerd.h>
struct resource_alloc_functions_s {
GHashTable *(*merge_weights) (resource_t *, const char *, GHashTable *, const char *, float,
enum pe_weights);
node_t *(*allocate) (resource_t *, node_t *, pe_working_set_t *);
void (*create_actions) (resource_t *, pe_working_set_t *);
gboolean(*create_probe) (resource_t *, node_t *, action_t *, gboolean, pe_working_set_t *);
void (*internal_constraints) (resource_t *, pe_working_set_t *);
void (*rsc_colocation_lh) (resource_t *, resource_t *, rsc_colocation_t *);
void (*rsc_colocation_rh) (resource_t *, resource_t *, rsc_colocation_t *);
void (*rsc_location) (pe_resource_t *, pe__location_t *);
enum pe_action_flags (*action_flags) (action_t *, node_t *);
enum pe_graph_flags (*update_actions) (action_t *, action_t *, node_t *, enum pe_action_flags,
enum pe_action_flags, enum pe_ordering);
void (*expand) (resource_t *, pe_working_set_t *);
void (*append_meta) (resource_t * rsc, xmlNode * xml);
};
extern GHashTable *rsc_merge_weights(resource_t * rsc, const char *rhs, GHashTable * nodes,
const char *attr, float factor, enum pe_weights flags);
extern GHashTable *clone_merge_weights(resource_t * rsc, const char *rhs, GHashTable * nodes,
const char *attr, float factor, enum pe_weights flags);
extern GHashTable *container_merge_weights(resource_t * rsc, const char *rhs, GHashTable * nodes,
const char *attr, float factor, enum pe_weights flags);
extern GHashTable *native_merge_weights(resource_t * rsc, const char *rhs, GHashTable * nodes,
const char *attr, float factor, enum pe_weights flags);
extern GHashTable *group_merge_weights(resource_t * rsc, const char *rhs, GHashTable * nodes,
const char *attr, float factor, enum pe_weights flags);
extern node_t *native_color(resource_t * rsc, node_t * preferred, pe_working_set_t * data_set);
extern void native_create_actions(resource_t * rsc, pe_working_set_t * data_set);
extern void native_internal_constraints(resource_t * rsc, pe_working_set_t * data_set);
extern void native_rsc_colocation_lh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern void native_rsc_colocation_rh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern void rsc_ticket_constraint(resource_t * lh_rsc, rsc_ticket_t * rsc_ticket,
pe_working_set_t * data_set);
extern enum pe_action_flags native_action_flags(action_t * action, node_t * node);
void native_rsc_location(pe_resource_t *rsc, pe__location_t *constraint);
extern void native_expand(resource_t * rsc, pe_working_set_t * data_set);
extern gboolean native_create_probe(resource_t * rsc, node_t * node, action_t * complete,
gboolean force, pe_working_set_t * data_set);
extern void native_append_meta(resource_t * rsc, xmlNode * xml);
extern node_t *group_color(resource_t * rsc, node_t * preferred, pe_working_set_t * data_set);
extern void group_create_actions(resource_t * rsc, pe_working_set_t * data_set);
extern void group_internal_constraints(resource_t * rsc, pe_working_set_t * data_set);
extern void group_rsc_colocation_lh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern void group_rsc_colocation_rh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern enum pe_action_flags group_action_flags(action_t * action, node_t * node);
void group_rsc_location(pe_resource_t *rsc, pe__location_t *constraint);
extern void group_expand(resource_t * rsc, pe_working_set_t * data_set);
extern void group_append_meta(resource_t * rsc, xmlNode * xml);
extern node_t *container_color(resource_t * rsc, node_t * preferred, pe_working_set_t * data_set);
extern void container_create_actions(resource_t * rsc, pe_working_set_t * data_set);
extern void container_internal_constraints(resource_t * rsc, pe_working_set_t * data_set);
extern void container_rsc_colocation_lh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern void container_rsc_colocation_rh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
void container_rsc_location(pe_resource_t *rsc, pe__location_t *constraint);
extern enum pe_action_flags container_action_flags(action_t * action, node_t * node);
extern void container_expand(resource_t * rsc, pe_working_set_t * data_set);
extern gboolean container_create_probe(resource_t * rsc, node_t * node, action_t * complete,
gboolean force, pe_working_set_t * data_set);
extern void container_append_meta(resource_t * rsc, xmlNode * xml);
extern node_t *clone_color(resource_t * rsc, node_t * preferred, pe_working_set_t * data_set);
extern void clone_create_actions(resource_t * rsc, pe_working_set_t * data_set);
extern void clone_internal_constraints(resource_t * rsc, pe_working_set_t * data_set);
extern void clone_rsc_colocation_lh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
extern void clone_rsc_colocation_rh(resource_t * lh_rsc, resource_t * rh_rsc,
rsc_colocation_t * constraint);
void clone_rsc_location(pe_resource_t *rsc, pe__location_t *constraint);
extern enum pe_action_flags clone_action_flags(action_t * action, node_t * node);
extern void clone_expand(resource_t * rsc, pe_working_set_t * data_set);
extern gboolean clone_create_probe(resource_t * rsc, node_t * node, action_t * complete,
gboolean force, pe_working_set_t * data_set);
extern void clone_append_meta(resource_t * rsc, xmlNode * xml);
void apply_master_prefs(resource_t *rsc);
node_t *color_promotable(resource_t *rsc, pe_working_set_t *data_set);
void create_promotable_actions(resource_t *rsc, pe_working_set_t *data_set);
void promote_demote_constraints(resource_t *rsc, pe_working_set_t *data_set);
void promotable_constraints(resource_t *rsc, pe_working_set_t *data_set);
void promotable_colocation_rh(resource_t *lh_rsc, resource_t *rh_rsc,
rsc_colocation_t *constraint);
/* extern resource_object_functions_t resource_variants[]; */
extern resource_alloc_functions_t resource_class_alloc_functions[];
gboolean is_active(pe__location_t *cons);
extern gboolean unpack_rsc_order(xmlNode * xml_obj, pe_working_set_t * data_set);
extern gboolean unpack_rsc_colocation(xmlNode * xml_obj, pe_working_set_t * data_set);
extern gboolean unpack_location(xmlNode * xml_obj, pe_working_set_t * data_set);
extern gboolean unpack_rsc_ticket(xmlNode * xml_obj, pe_working_set_t * data_set);
void LogNodeActions(pe_working_set_t * data_set, gboolean terminal);
void LogActions(resource_t * rsc, pe_working_set_t * data_set, gboolean terminal);
void container_LogActions(resource_t * rsc, pe_working_set_t * data_set, gboolean terminal);
extern void rsc_stonith_ordering(resource_t * rsc, action_t * stonith_op,
pe_working_set_t * data_set);
extern enum pe_graph_flags native_update_actions(action_t * first, action_t * then, node_t * node,
enum pe_action_flags flags,
enum pe_action_flags filter,
enum pe_ordering type);
extern enum pe_graph_flags group_update_actions(action_t * first, action_t * then, node_t * node,
enum pe_action_flags flags,
enum pe_action_flags filter, enum pe_ordering type);
extern enum pe_graph_flags container_update_actions(action_t * first, action_t * then, node_t * node,
enum pe_action_flags flags,
enum pe_action_flags filter, enum pe_ordering type);
gboolean update_action_flags(action_t * action, enum pe_action_flags flags, const char *source, int line);
gboolean update_action(action_t * action);
void complex_set_cmds(resource_t * rsc);
void clone_create_pseudo_actions(
resource_t * rsc, GListPtr children, notify_data_t **start_notify, notify_data_t **stop_notify, pe_working_set_t * data_set);
+void libpengine_fini(void);
#endif
diff --git a/daemons/schedulerd/sched_messages.c b/daemons/schedulerd/sched_messages.c
index 489a980bce..2bf7ec3edd 100644
--- a/daemons/schedulerd/sched_messages.c
+++ b/daemons/schedulerd/sched_messages.c
@@ -1,290 +1,302 @@
/*
* Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <crm/crm.h>
#include <crm/cib.h>
#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <glib.h>
#include <crm/pengine/status.h>
#include <pacemaker-schedulerd.h>
#include <sched_allocate.h>
#include <sched_utils.h>
#include <crm/common/ipcs.h>
xmlNode *do_calculations(pe_working_set_t * data_set, xmlNode * xml_input, crm_time_t * now);
gboolean show_scores = FALSE;
int scores_log_level = LOG_TRACE;
gboolean show_utilization = FALSE;
int utilization_log_level = LOG_TRACE;
extern int transition_id;
+static pe_working_set_t *sched_data_set = NULL;
+
#define get_series() was_processing_error?1:was_processing_warning?2:3
typedef struct series_s {
const char *name;
const char *param;
int wrap;
} series_t;
series_t series[] = {
{"pe-unknown", "_dont_match_anything_", -1},
{"pe-error", "pe-error-series-max", -1},
{"pe-warn", "pe-warn-series-max", 200},
{"pe-input", "pe-input-series-max", 400},
};
gboolean process_pe_message(xmlNode * msg, xmlNode * xml_data, crm_client_t * sender);
gboolean
process_pe_message(xmlNode * msg, xmlNode * xml_data, crm_client_t * sender)
{
static char *last_digest = NULL;
static char *filename = NULL;
time_t execution_date = time(NULL);
const char *sys_to = crm_element_value(msg, F_CRM_SYS_TO);
const char *op = crm_element_value(msg, F_CRM_TASK);
const char *ref = crm_element_value(msg, F_CRM_REFERENCE);
crm_trace("Processing %s op (ref=%s)...", op, ref);
if (op == NULL) {
/* error */
} else if (strcasecmp(op, CRM_OP_HELLO) == 0) {
/* ignore */
} else if (safe_str_eq(crm_element_value(msg, F_CRM_MSG_TYPE), XML_ATTR_RESPONSE)) {
/* ignore */
} else if (sys_to == NULL || strcasecmp(sys_to, CRM_SYSTEM_PENGINE) != 0) {
crm_trace("Bad sys-to %s", crm_str(sys_to));
return FALSE;
} else if (strcasecmp(op, CRM_OP_PECALC) == 0) {
int seq = -1;
int series_id = 0;
int series_wrap = 0;
char *digest = NULL;
const char *value = NULL;
- pe_working_set_t data_set;
xmlNode *converted = NULL;
xmlNode *reply = NULL;
gboolean is_repoke = FALSE;
gboolean process = TRUE;
crm_config_error = FALSE;
crm_config_warning = FALSE;
was_processing_error = FALSE;
was_processing_warning = FALSE;
- set_working_set_defaults(&data_set);
+ if (sched_data_set == NULL) {
+ sched_data_set = pe_new_working_set();
+ CRM_ASSERT(sched_data_set != NULL);
+ }
digest = calculate_xml_versioned_digest(xml_data, FALSE, FALSE, CRM_FEATURE_SET);
converted = copy_xml(xml_data);
if (cli_config_update(&converted, NULL, TRUE) == FALSE) {
- data_set.graph = create_xml_node(NULL, XML_TAG_GRAPH);
- crm_xml_add_int(data_set.graph, "transition_id", 0);
- crm_xml_add_int(data_set.graph, "cluster-delay", 0);
+ sched_data_set->graph = create_xml_node(NULL, XML_TAG_GRAPH);
+ crm_xml_add_int(sched_data_set->graph, "transition_id", 0);
+ crm_xml_add_int(sched_data_set->graph, "cluster-delay", 0);
process = FALSE;
free(digest);
} else if (safe_str_eq(digest, last_digest)) {
crm_info("Input has not changed since last time, not saving to disk");
is_repoke = TRUE;
free(digest);
} else {
free(last_digest);
last_digest = digest;
}
if (process) {
- do_calculations(&data_set, converted, NULL);
+ do_calculations(sched_data_set, converted, NULL);
}
series_id = get_series();
series_wrap = series[series_id].wrap;
- value = pe_pref(data_set.config_hash, series[series_id].param);
+ value = pe_pref(sched_data_set->config_hash, series[series_id].param);
if (value != NULL) {
series_wrap = crm_int_helper(value, NULL);
if (errno != 0) {
series_wrap = series[series_id].wrap;
}
} else {
crm_config_warn("No value specified for cluster"
" preference: %s", series[series_id].param);
}
seq = get_last_sequence(PE_STATE_DIR, series[series_id].name);
crm_trace("Series %s: wrap=%d, seq=%d, pref=%s",
series[series_id].name, series_wrap, seq, value);
- data_set.input = NULL;
- reply = create_reply(msg, data_set.graph);
+ sched_data_set->input = NULL;
+ reply = create_reply(msg, sched_data_set->graph);
CRM_ASSERT(reply != NULL);
if (is_repoke == FALSE) {
free(filename);
filename =
generate_series_filename(PE_STATE_DIR, series[series_id].name, seq, HAVE_BZLIB_H);
}
crm_xml_add(reply, F_CRM_TGRAPH_INPUT, filename);
crm_xml_add_int(reply, "graph-errors", was_processing_error);
crm_xml_add_int(reply, "graph-warnings", was_processing_warning);
crm_xml_add_int(reply, "config-errors", crm_config_error);
crm_xml_add_int(reply, "config-warnings", crm_config_warning);
if (crm_ipcs_send(sender, 0, reply, crm_ipc_server_event) == FALSE) {
int graph_file_fd = 0;
char *graph_file = NULL;
umask(S_IWGRP | S_IWOTH | S_IROTH);
graph_file = crm_strdup_printf("%s/pengine.graph.XXXXXX",
PE_STATE_DIR);
graph_file_fd = mkstemp(graph_file);
crm_err("Couldn't send transition graph to peer, writing to %s instead",
graph_file);
crm_xml_add(reply, F_CRM_TGRAPH, graph_file);
- write_xml_fd(data_set.graph, graph_file, graph_file_fd, FALSE);
+ write_xml_fd(sched_data_set->graph, graph_file, graph_file_fd, FALSE);
free(graph_file);
free_xml(first_named_child(reply, F_CRM_DATA));
CRM_ASSERT(crm_ipcs_send(sender, 0, reply, crm_ipc_server_event));
}
free_xml(reply);
- pe_reset_working_set(&data_set);
+ pe_reset_working_set(sched_data_set);
if (was_processing_error) {
crm_err("Calculated transition %d (with errors), saving inputs in %s",
transition_id, filename);
} else if (was_processing_warning) {
crm_warn("Calculated transition %d (with warnings), saving inputs in %s",
transition_id, filename);
} else {
crm_notice("Calculated transition %d, saving inputs in %s",
transition_id, filename);
}
if (crm_config_error) {
crm_notice("Configuration errors found during scheduler processing,"
" please run \"crm_verify -L\" to identify issues");
}
if (is_repoke == FALSE && series_wrap != 0) {
unlink(filename);
crm_xml_add_int(xml_data, "execution-date", execution_date);
write_xml_file(xml_data, filename, HAVE_BZLIB_H);
write_last_sequence(PE_STATE_DIR, series[series_id].name, seq + 1, series_wrap);
} else {
crm_trace("Not writing out %s: %d & %d", filename, is_repoke, series_wrap);
}
free_xml(converted);
}
return TRUE;
}
+// only needed if process_pe_message() is called
+void
+libpengine_fini()
+{
+ pe_free_working_set(sched_data_set);
+ sched_data_set = NULL;
+}
+
xmlNode *
do_calculations(pe_working_set_t * data_set, xmlNode * xml_input, crm_time_t * now)
{
GListPtr gIter = NULL;
int rsc_log_level = LOG_INFO;
/* pe_debug_on(); */
CRM_ASSERT(xml_input || is_set(data_set->flags, pe_flag_have_status));
if (is_set(data_set->flags, pe_flag_have_status) == FALSE) {
set_working_set_defaults(data_set);
data_set->input = xml_input;
data_set->now = now;
} else {
crm_trace("Already have status - reusing");
}
if (data_set->now == NULL) {
data_set->now = crm_time_new(NULL);
}
crm_trace("Calculate cluster status");
stage0(data_set);
if(is_not_set(data_set->flags, pe_flag_quick_location)) {
gIter = data_set->resources;
for (; gIter != NULL; gIter = gIter->next) {
resource_t *rsc = (resource_t *) gIter->data;
if (is_set(rsc->flags, pe_rsc_orphan) && rsc->role == RSC_ROLE_STOPPED) {
continue;
}
rsc->fns->print(rsc, NULL, pe_print_log, &rsc_log_level);
}
}
crm_trace("Applying placement constraints");
stage2(data_set);
if(is_set(data_set->flags, pe_flag_quick_location)){
return NULL;
}
crm_trace("Create internal constraints");
stage3(data_set);
crm_trace("Check actions");
stage4(data_set);
crm_trace("Allocate resources");
stage5(data_set);
crm_trace("Processing fencing and shutdown cases");
stage6(data_set);
crm_trace("Applying ordering constraints");
stage7(data_set);
crm_trace("Create transition graph");
stage8(data_set);
crm_trace("=#=#=#=#= Summary =#=#=#=#=");
crm_trace("\t========= Set %d (Un-runnable) =========", -1);
if (get_crm_log_level() >= LOG_TRACE) {
gIter = data_set->actions;
for (; gIter != NULL; gIter = gIter->next) {
action_t *action = (action_t *) gIter->data;
if (is_set(action->flags, pe_action_optional) == FALSE
&& is_set(action->flags, pe_action_runnable) == FALSE
&& is_set(action->flags, pe_action_pseudo) == FALSE) {
log_action(LOG_TRACE, "\t", action, TRUE);
}
}
}
return data_set->graph;
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 16, 12:17 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2530778
Default Alt Text
(24 KB)

Event Timeline