diff --git a/include/Makefile.am b/include/Makefile.am index 25a6d75fc..17f7d1cea 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -47,6 +47,7 @@ nobase_include_HEADERS = \ osmocom/core/statistics.h \ osmocom/core/strrb.h \ osmocom/core/talloc.h \ + osmocom/core/tdef.h \ osmocom/core/timer.h \ osmocom/core/timer_compat.h \ osmocom/core/utils.h \ @@ -154,6 +155,7 @@ nobase_include_HEADERS += \ osmocom/vty/vector.h \ osmocom/vty/vty.h \ osmocom/vty/ports.h \ + osmocom/vty/tdef_vty.h \ osmocom/ctrl/control_vty.h endif diff --git a/include/osmocom/core/tdef.h b/include/osmocom/core/tdef.h new file mode 100644 index 000000000..92b71597b --- /dev/null +++ b/include/osmocom/core/tdef.h @@ -0,0 +1,172 @@ +/*! \file tdef.h + * API to define Tnnn timers globally and use for FSM state changes. + */ +/* + * (C) 2018-2019 by sysmocom - s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include + +struct osmo_fsm_inst; + +/*! \defgroup Tdef Tnnn timer configuration + * @{ + * \file tdef.h + */ + +enum osmo_tdef_unit { + OSMO_TDEF_S = 0, /*!< most T are in seconds, keep 0 as default. */ + OSMO_TDEF_MS, /*!< milliseconds */ + OSMO_TDEF_M, /*!< minutes */ + OSMO_TDEF_CUSTOM, /*!< unspecified unit, explained in osmo_tdef.desc. */ +}; + +extern const struct value_string osmo_tdef_unit_names[]; +/*! \return enum osmo_tdef_unit value as human readable unit letter, or "custom-unit". */ +static inline const char *osmo_tdef_unit_name(enum osmo_tdef_unit val) +{ return get_value_string(osmo_tdef_unit_names, val); } + +/*! Define a GSM timer of the form Tnnn, with unit, default value and doc string. + * Typically used as an array with the last entry being left zero-initialized, e.g.: + * + * struct osmo_tdef tdefs[] = { + * { .T=10, .default_val=6, .desc="RR Assignment" }, + * { .T=101, .default_val=10, .desc="inter-BSC Handover MT, HO Request to HO Accept" }, + * { .T=3101, .default_val=3, .desc="RR Immediate Assignment" }, + * {} + * }; + * + * Program initialization should call osmo_tdefs_reset() so that all timers return the default_val, until e.g. the VTY + * configuration sets user-defined values (see osmo_tdef_vty_init()). + */ +struct osmo_tdef { + /*! T1234 number; type corresponds to struct osmo_fsm_inst.T. Negative and zero T numbers are actually possible, + * but be aware that osmo_tdef_fsm_inst_state_chg() interprets T == 0 as "no timer". */ + const int T; + /*! Timeout duration (according to unit), default value; type corresponds to osmo_fsm_inst_state_chg()'s + * timeout_secs argument. Note that osmo_fsm_inst_state_chg() clamps the range. */ + const unsigned long default_val; + const enum osmo_tdef_unit unit; + /*! Human readable description. For unit == OSMO_TDEF_CUSTOM, this should include an explanation of the value's + * unit. Best keep this a short one-liner (e.g. for VTY output). */ + const char *desc; + /*! Currently active timeout value, e.g. set by user config. This is the only mutable member: a user may + * configure the timeout value, but neither unit nor any other field. */ + unsigned long val; +}; + +/*! Iterate an array of struct osmo_tdef, the last item should be fully zero, i.e. "{}". + * Example: + * + * struct osmo_tdef *t; + * osmo_tdef_for_each(t, tdefs) { + * printf("%lu %s %s\n", t->val, osmo_tdef_unit_name(t->unit), t->desc); + * } + * + * \param[inout] t A struct osmo_tdef *t used for iteration, will point at the current entry inside the loop scope. + * \param[in] tdefs Array of struct osmo_tdef to iterate, zero-terminated. + */ +#define osmo_tdef_for_each(t, tdefs) \ + for (t = tdefs; t && (t->T || t->default_val || t->desc); t++) + +void osmo_tdefs_reset(struct osmo_tdef *tdefs); +unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, + unsigned long val_if_not_present); +struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T); + +/*! Using osmo_tdef for osmo_fsm_inst: array entry for a mapping of state numbers to timeout definitions. + * For a usage example, see osmo_tdef_get_state_timeout() and test_tdef_state_timeout() in tdef_test.c. */ +struct osmo_tdef_state_timeout { + /*! Timer number to match struct osmo_tdef.T, and to pass to osmo_fsm_inst_state_chg(). */ + int T; + /*! If true, call osmo_fsm_inst_state_chg_keep_timer(). + * If T == 0, keep previous T number, otherwise also set fi->T. */ + bool keep_timer; +}; + +const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array); + +/*! Call osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer(), depending on the timeouts_array, tdefs and + * default_timeout. + * + * A T timer configured in sub-second precision is rounded up to the next full second. A timer in unit = + * OSMO_TDEF_CUSTOM is applied as if the unit is in seconds (i.e. this macro does not make sense for custom units!). + * + * See osmo_tdef_get_state_timeout() and osmo_tdef_get(). + * + * If no T timer is defined for the given state (T == 0), invoke the state change without a timeout. + * + * Should a T number be defined in timeouts_array that is not defined in tdefs, use default_timeout (in seconds). If + * default_timeout is negative, a missing T definition in tdefs instead causes a program abort. + * + * This is best used by wrapping this function call in a macro suitable for a specific FSM implementation, which can + * become as short as: my_fsm_state_chg(fi, NEXT_STATE): + * + * #define my_fsm_state_chg(fi, NEXT_STATE) \ + * osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, my_fsm_timeouts, global_T_defs, 5) + * + * my_fsm_state_chg(fi, MY_FSM_STATE_1); + * // -> No timeout configured, will enter state without timeout. + * + * my_fsm_state_chg(fi, MY_FSM_STATE_3); + * // T423 configured for this state, will look up T423 in tdefs, or use 5 seconds if unset. + * + * my_fsm_state_chg(fi, MY_FSM_STATE_8); + * // keep_timer == true for this state, will invoke osmo_fsm_inst_state_chg_keep_timer(). + * + * \param[inout] fi osmo_fsm_inst to transition to another state. + * \param[in] state State number to transition to. + * \param[in] timeouts_array Array of struct osmo_tdef_state_timeout[32] to look up state in. + * \param[in] tdefs Array of struct osmo_tdef (last entry zero initialized) to look up T in. + * \param[in] default_timeout If a T is set in timeouts_array, but no timeout value is configured for T, then use this + * default timeout value as fallback, or pass -1 to abort the program. + * \return Return value from osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer(). + */ +#define osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout) \ + _osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout, \ + __FILE__, __LINE__) +int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array, + const struct osmo_tdef *tdefs, unsigned long default_timeout, + const char *file, int line); + +/*! Manage timer definitions in named groups. + * This should be defined as an array with the final element kept fully zero-initialized, + * to be compatible with osmo_tdef_vty* API. There must not be any tdefs == NULL entries except on the final + * zero-initialized entry. */ +struct osmo_tdef_group { + const char *name; + const char *desc; + struct osmo_tdef *tdefs; +}; + +/*! Iterate an array of struct osmo_tdef_group, the last item should be fully zero, i.e. "{}". + * \param[inout] g A struct osmo_tdef_group *g used for iteration, will point at the current entry inside the loop scope. + * \param[in] tdefs Array of struct osmo_tdef_group to iterate, zero-terminated. + */ +#define osmo_tdef_groups_for_each(g, tdef_groups) \ + for (g = tdef_groups; g && g->tdefs; g++) + +/*! @} */ diff --git a/include/osmocom/vty/tdef_vty.h b/include/osmocom/vty/tdef_vty.h new file mode 100644 index 000000000..f55239adf --- /dev/null +++ b/include/osmocom/vty/tdef_vty.h @@ -0,0 +1,67 @@ +/*! \file tdef_vty.h + * API to configure osmo_tdef Tnnn timers from VTY configuration. + */ +/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH + * + * Author: Neels Hofmeyr + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +struct vty; + +/*! \defgroup Tdef_VTY Tnnn timer VTY configuration + * @{ + * \file tdef_vty.h + */ + +struct osmo_tdef; +struct osmo_tdef_group; + +#define OSMO_TDEF_VTY_ARG_T "TNNNN" +#define OSMO_TDEF_VTY_DOC_T "T-number, optionally preceded by 't' or 'T'.\n" +#define OSMO_TDEF_VTY_ARG_T_OPTIONAL "[" OSMO_TDEF_VTY_ARG_T "]" + +#define OSMO_TDEF_VTY_ARG_VAL "(<0-2147483647>|default)" +#define OSMO_TDEF_VTY_DOC_VAL "New timer value\n" "Set to default timer value\n" +#define OSMO_TDEF_VTY_ARG_VAL_OPTIONAL "[" OSMO_TDEF_VTY_ARG_VAL "]" + +#define OSMO_TDEF_VTY_ARG_SET OSMO_TDEF_VTY_ARG_T " " OSMO_TDEF_VTY_ARG_VAL +#define OSMO_TDEF_VTY_DOC_SET OSMO_TDEF_VTY_DOC_T OSMO_TDEF_VTY_DOC_VAL +#define OSMO_TDEF_VTY_ARG_SET_OPTIONAL OSMO_TDEF_VTY_ARG_T_OPTIONAL " " OSMO_TDEF_VTY_ARG_VAL_OPTIONAL + +int osmo_tdef_vty_set_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char **args); +int osmo_tdef_vty_show_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char *T_arg, + const char *prefix_fmt, ...); +void osmo_tdef_vty_write(struct vty *vty, struct osmo_tdef *tdefs, + const char *prefix_fmt, ...); + +void osmo_tdef_vty_out_one(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, ...); +void osmo_tdef_vty_out_all(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...); + +void osmo_tdef_vty_out_one_va(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, va_list va); +void osmo_tdef_vty_out_all_va(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, va_list va); + +struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, struct osmo_tdef *tdefs, const char *osmo_tdef_str); +unsigned long osmo_tdef_vty_parse_val_arg(const char *val_arg, unsigned long default_val); + +void osmo_tdef_vty_groups_init(enum node_type parent_node, struct osmo_tdef_group *groups); +void osmo_tdef_vty_groups_write(struct vty *vty, const char *indent); + +/*! @} */ diff --git a/src/Makefile.am b/src/Makefile.am index 6840f7989..27ab70261 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -23,7 +23,8 @@ libosmocore_la_SOURCES = timer.c timer_gettimeofday.c timer_clockgettime.c \ loggingrb.c crc8gen.c crc16gen.c crc32gen.c crc64gen.c \ macaddr.c stat_item.c stats.c stats_statsd.c prim.c \ conv_acc.c conv_acc_generic.c sercomm.c prbs.c \ - isdnhdlc.c + isdnhdlc.c \ + tdef.c if HAVE_SSSE3 libosmocore_la_SOURCES += conv_acc_sse.c diff --git a/src/fsm.c b/src/fsm.c index 0d31f8728..6e15ab70c 100644 --- a/src/fsm.c +++ b/src/fsm.c @@ -498,6 +498,10 @@ static int state_chg(struct osmo_fsm_inst *fi, uint32_t new_state, * timer_cb. If passing timeout_secs == 0, it is recommended to also pass T == * 0, so that fi->T is reset to 0 when no timeout is invoked. * + * See also osmo_tdef_fsm_inst_state_chg() from the osmo_tdef API, which + * provides a unified way to configure and apply GSM style Tnnnn timers to FSM + * state transitions. + * * Range: since time_t's maximum value is not well defined in a cross platform * way, clamp timeout_secs to the maximum of the signed 32bit range, or roughly * 68 years (float(0x7fffffff) / (60. * 60 * 24 * 365.25) = 68.0497). Thus diff --git a/src/tdef.c b/src/tdef.c new file mode 100644 index 000000000..7e79d6804 --- /dev/null +++ b/src/tdef.c @@ -0,0 +1,282 @@ +/*! \file tdef.c + * Implementation to define Tnnn timers globally and use for FSM state changes. + */ +/* + * (C) 2018-2019 by sysmocom - s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include + +/*! \addtogroup Tdef + * + * Implementation to define Tnnn timers globally and use for FSM state changes. + * + * See also \ref Tdef_VTY + * + * osmo_tdef provides: + * + * - a list of Tnnnn (GSM) timers with description, unit and default value. + * - vty UI to allow users to configure non-default timeouts. + * - API to tie T timers to osmo_fsm states and set them on state transitions. + * + * - a few standard units (minute, second, millisecond) as well as a custom unit + * (which relies on the timer's human readable description to indicate the + * meaning of the value). + * - conversion for standard units: for example, some GSM timers are defined in + * minutes, while our FSM definitions need timeouts in seconds. Conversion is + * for convenience only and can be easily avoided via the custom unit. + * + * By keeping separate osmo_tdef arrays, several groups of timers can be kept + * separately. The VTY tests in tests/tdef/ showcase different schemes: + * + * - \ref tests/vty/tdef_vty_test_config_root.c: + * Keep several timer definitions in separately named groups: showcase the + * osmo_tdef_vty_groups*() API. Each timer group exists exactly once. + * + * - \ref tests/vty/tdef_vty_test_config_subnode.c: + * Keep a single list of timers without separate grouping. + * Put this list on a specific subnode below the CONFIG_NODE. + * There could be several separate subnodes with timers like this, i.e. + * continuing from this example, sets of timers could be separated by placing + * timers in specific config subnodes instead of using the global group name. + * + * - \ref tests/vty/tdef_vty_test_dynamic.c: + * Dynamically allocate timer definitions per each new created object. + * Thus there can be an arbitrary number of independent timer definitions, one + * per allocated object. + * + * osmo_tdef was introduced because: + * + * - without osmo_tdef, each invocation of osmo_fsm_inst_state_chg() needs to be + * programmed with the right timeout value, for all code paths that invoke this + * state change. It is a likely source of errors to get one of them wrong. By + * defining a T timer exactly for an FSM state, the caller can merely invoke the + * state change and trust on the original state definition to apply the correct + * timeout. + * + * - it is helpful to have a standardized config file UI to provide user + * configurable timeouts, instead of inventing new VTY commands for each + * separate application of T timer numbers. See \ref tdef_vty.h. + * + * @{ + * \file tdef.c + */ + +/*! a = return_val * b. \return 0 if factor is below 1. */ +static unsigned long osmo_tdef_factor(enum osmo_tdef_unit a, enum osmo_tdef_unit b) +{ + if (b == a + || b == OSMO_TDEF_CUSTOM || a == OSMO_TDEF_CUSTOM) + return 1; + + switch (b) { + case OSMO_TDEF_MS: + switch (a) { + case OSMO_TDEF_S: + return 1000; + case OSMO_TDEF_M: + return 60*1000; + default: + return 0; + } + case OSMO_TDEF_S: + switch (a) { + case OSMO_TDEF_M: + return 60; + default: + return 0; + } + default: + return 0; + } +} + +/*! \return val in unit to_unit, rounded up to the next integer value and clamped to ULONG_MAX, or 0 if val == 0. */ +static unsigned long osmo_tdef_round(unsigned long val, enum osmo_tdef_unit from_unit, enum osmo_tdef_unit to_unit) +{ + unsigned long f; + if (!val) + return 0; + + f = osmo_tdef_factor(from_unit, to_unit); + if (f == 1) + return val; + if (f < 1) { + f = osmo_tdef_factor(to_unit, from_unit); + return (val / f) + (val % f? 1 : 0); + } + /* range checking */ + if (f > (ULONG_MAX / val)) + return ULONG_MAX; + return val * f; +} + +/*! Set all osmo_tdef values to the default_val. + * It is convenient to define a tdefs array by setting only the default_val, and calling osmo_tdefs_reset() once for + * program startup. (See also osmo_tdef_vty_init()) + * \param[in] tdefs Array of timer definitions, last entry being fully zero. + */ +void osmo_tdefs_reset(struct osmo_tdef *tdefs) +{ + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) + t->val = t->default_val; +} + +/*! Return the value of a T timer from a list of osmo_tdef, in the given unit. + * If no such timer is defined, return the default value passed, or abort the program if default < 0. + * + * Round up any value match as_unit: 1100 ms as OSMO_TDEF_S becomes 2 seconds, as OSMO_TDEF_M becomes one minute. + * However, always return a value of zero as zero (0 ms as OSMO_TDEF_M still is 0 m). + * + * Range: even though the value range is unsigned long here, in practice, using ULONG_MAX as value for a timeout in + * seconds may actually wrap to negative or low timeout values (e.g. in struct timeval). It is recommended to stay below + * INT_MAX seconds. See also osmo_fsm_inst_state_chg(). + * + * Usage example: + * + * struct osmo_tdef global_T_defs[] = { + * { .T=7, .default_val=50, .desc="Water Boiling Timeout" }, // default is .unit=OSMO_TDEF_S == 0 + * { .T=8, .default_val=300, .desc="Tea brewing" }, + * { .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" }, + * { .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" }, + * {} // <-- important! last entry shall be zero + * }; + * osmo_tdefs_reset(global_T_defs); // make all values the default + * osmo_tdef_vty_init(global_T_defs, CONFIG_NODE); + * + * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_S, -1); // -> 50 + * sleep(val); + * + * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_M, -1); // 50 seconds becomes 1 minute -> 1 + * sleep_minutes(val); + * + * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, 3); // not defined, returns 3 + * + * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, -1); // not defined, program aborts! + * + * \param[in] tdefs Array of timer definitions, last entry must be fully zero initialized. + * \param[in] T Timer number to get the value for. + * \param[in] as_unit Return timeout value in this unit. + * \param[in] val_if_not_present Fallback value to return if no timeout is defined. + * \return Timeout value in the unit given by as_unit, rounded up if necessary, or val_if_not_present. + */ +unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, unsigned long val_if_not_present) +{ + const struct osmo_tdef *t = osmo_tdef_get_entry((struct osmo_tdef*)tdefs, T); + if (!t) { + OSMO_ASSERT(val_if_not_present >= 0); + return val_if_not_present; + } + return osmo_tdef_round(t->val, t->unit, as_unit); +} + +/*! Find tdef entry matching T. + * This is useful for manipulation, which is usually limited to the VTY configuration. To retrieve a timeout value, + * most callers probably should use osmo_tdef_get() instead. + * \param[in] tdefs Array of timer definitions, last entry being fully zero. + * \param[in] T Timer number to get the entry for. + * \return osmo_tdef entry matching T in given array, or NULL if no match is found. + */ +struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T) +{ + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) { + if (t->T == T) + return t; + } + return NULL; +} + +/*! Using osmo_tdef for osmo_fsm_inst: find a given state's osmo_tdef_state_timeout entry. + * + * The timeouts_array shall contain exactly 32 elements, regardless whether only some of them are actually populated + * with nonzero values. 32 corresponds to the number of states allowed by the osmo_fsm_* API. Lookup is by array index. + * Not populated entries imply a state change invocation without timeout. + * + * For example: + * + * struct osmo_tdef_state_timeout my_fsm_timeouts[32] = { + * [MY_FSM_STATE_3] = { .T = 423 }, // look up timeout configured for T423 + * [MY_FSM_STATE_7] = { .T = 235 }, + * [MY_FSM_STATE_8] = { .keep_timer = true }, // keep previous state's T number, continue timeout. + * // any state that is omitted will remain zero == no timeout + * }; + * osmo_tdef_get_state_timeout(MY_FSM_STATE_0, &my_fsm_timeouts) -> NULL, + * osmo_tdef_get_state_timeout(MY_FSM_STATE_7, &my_fsm_timeouts) -> { .T = 235 } + * + * The intention is then to obtain the timer like osmo_tdef_get(global_T_defs, T=235); see also + * fsm_inst_state_chg_T() below. + * + * \param[in] state State constant to look up. + * \param[in] timeouts_array Array[32] of struct osmo_tdef_state_timeout defining which timer number to use per state. + * \return A struct osmo_tdef_state_timeout entry, or NULL if that entry is zero initialized. + */ +const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, const struct osmo_tdef_state_timeout *timeouts_array) +{ + const struct osmo_tdef_state_timeout *t; + OSMO_ASSERT(state < 32); + t = &timeouts_array[state]; + if (!t->keep_timer && !t->T) + return NULL; + return t; +} + +/*! See invocation macro osmo_tdef_fsm_inst_state_chg() instead. + * \param[in] file Source file name, like __FILE__. + * \param[in] line Source file line number, like __LINE__. + */ +int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array, + const struct osmo_tdef *tdefs, unsigned long default_timeout, + const char *file, int line) +{ + const struct osmo_tdef_state_timeout *t = osmo_tdef_get_state_timeout(state, timeouts_array); + unsigned long val; + + /* No timeout defined for this state? */ + if (!t) + return _osmo_fsm_inst_state_chg(fi, state, 0, 0, file, line); + + if (t->keep_timer) { + int rc = _osmo_fsm_inst_state_chg_keep_timer(fi, state, file, line); + if (t->T && !rc) + fi->T = t->T; + return rc; + } + + val = osmo_tdef_get(tdefs, t->T, OSMO_TDEF_S, default_timeout); + return _osmo_fsm_inst_state_chg(fi, state, val, t->T, file, line); +} + +const struct value_string osmo_tdef_unit_names[] = { + { OSMO_TDEF_S, "s" }, + { OSMO_TDEF_MS, "ms" }, + { OSMO_TDEF_M, "m" }, + { OSMO_TDEF_CUSTOM, "custom-unit" }, + {} +}; + +/*! @} */ diff --git a/src/vty/Makefile.am b/src/vty/Makefile.am index 2e4949821..cdde0fa5b 100644 --- a/src/vty/Makefile.am +++ b/src/vty/Makefile.am @@ -11,7 +11,8 @@ lib_LTLIBRARIES = libosmovty.la libosmovty_la_SOURCES = buffer.c command.c vty.c vector.c utils.c \ telnet_interface.c logging_vty.c stats_vty.c \ - fsm_vty.c talloc_ctx_vty.c + fsm_vty.c talloc_ctx_vty.c \ + tdef_vty.c libosmovty_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined libosmovty_la_LIBADD = $(top_builddir)/src/libosmocore.la $(TALLOC_LIBS) endif diff --git a/src/vty/tdef_vty.c b/src/vty/tdef_vty.c new file mode 100644 index 000000000..1c6af70a3 --- /dev/null +++ b/src/vty/tdef_vty.c @@ -0,0 +1,372 @@ +/*! \file tdef_vty.c + * Implementation to configure osmo_tdef Tnnn timers from VTY configuration. + */ +/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH + * + * Author: Neels Hofmeyr + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +/*! \addtogroup Tdef_VTY + * + * VTY API for \ref Tdef. + * + * @{ + * \file tdef_vty.c + */ + +/*! Parse an argument like "T1234", "t1234" or "1234", as from OSMO_TDEF_VTY_ARG_T. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions to look up T timer. + * \param[in] T_str Argument string. It is not validated, expected to be checked by VTY input. + * \return the corresponding osmo_tdef entry from the tdefs array, or NULL if no such entry exists. + */ +struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, struct osmo_tdef *tdefs, const char *T_str) +{ + long l; + int T; + struct osmo_tdef *t; + char *endptr; + const char *T_nr_str; + + if (!tdefs) { + vty_out(vty, "%% Error: no timers found%s", VTY_NEWLINE); + return NULL; + } + + T_nr_str = T_str; + if (T_nr_str[0] == 't' || T_nr_str[0] == 'T') + T_nr_str++; + + errno = 0; + l = strtol(T_nr_str, &endptr, 10); + if (errno || *endptr || l > INT_MAX) { + vty_out(vty, "%% No such timer: '%s'%s", T_str, VTY_NEWLINE); + return NULL; + } + T = l; + + t = osmo_tdef_get_entry(tdefs, T); + if (!t) + vty_out(vty, "%% No such timer: T%d%s", T, VTY_NEWLINE); + return t; +} + +/*! Parse an argument of the form "(0-2147483647|default)", as from OSMO_TDEF_VTY_ARG_VAL. + * \param[in] val_arg Argument string (not format checked). + * \param[in] default_val Value to return in case of val_arg being "default". + * \return Parsed value or default_val. + */ +unsigned long osmo_tdef_vty_parse_val_arg(const char *val_arg, unsigned long default_val) +{ + if (!strcmp(val_arg, "default")) + return default_val; + return atoll(val_arg); +} + +/*! Apply a timer configuration from VTY argument strings. + * Employ both osmo_tdef_vty_parse_T_arg() and osmo_tdef_vty_parse_val_arg() to configure a T timer in an array of + * tdefs. Evaluate two arguments, a "T1234" argument and a "(0-2147483647|default)" argument, as from + * OSMO_TDEF_VTY_ARGS. If the T timer given in the first argument is found in tdefs, set it to the value given in the + * second argument. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions to look up T timer. + * \param[in] args Array of string arguments like { "T1234", "23" }. + * \return CMD_SUCCESS, or CMD_WARNING if no such timer is found in tdefs. + */ +int osmo_tdef_vty_set_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char **args) +{ + const char *T_arg = args[0]; + const char *val_arg = args[1]; + struct osmo_tdef *t = osmo_tdef_vty_parse_T_arg(vty, tdefs, T_arg); + if (!t) + return CMD_WARNING; + t->val = osmo_tdef_vty_parse_val_arg(val_arg, t->default_val); + return CMD_SUCCESS; +} + +/*! Output one or all timers to the VTY, as for a VTY command like 'show timer [TNNNN]'. + * If T_arg is NULL, print all timers in tdefs to the VTY. + * If T_arg is not NULL, employ osmo_tdef_vty_parse_T_arg() to select one timer from tdefs and print only that to the + * VTY. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions. + * \param[in] T_arg Argument string like "T1234", or NULL. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + * \return CMD_SUCCESS, or CMD_WARNING if no such timer is found in tdefs. + */ +int osmo_tdef_vty_show_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char *T_arg, + const char *prefix_fmt, ...) +{ + va_list va; + if (T_arg) { + struct osmo_tdef *t = osmo_tdef_vty_parse_T_arg(vty, tdefs, T_arg); + if (!t) + return CMD_WARNING; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va); + } else { + va_start(va, prefix_fmt); + osmo_tdef_vty_out_all_va(vty, tdefs, prefix_fmt, va); + va_end(va); + } + return CMD_SUCCESS; +} + +/*! Write to VTY the current status of one timer. + * \param[in] vty VTY context for vty_out(). + * \param[in] t The timer to print. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable vprintf like arguments. + * \param[in] va va_list instance. As always, call va_start() before, and va_end() after this call. + */ +void osmo_tdef_vty_out_one_va(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, va_list va) +{ + if (!t) { + vty_out(vty, "%% Error: no such timer%s", VTY_NEWLINE); + return; + } + if (prefix_fmt) + vty_out_va(vty, prefix_fmt, va); + vty_out(vty, "T%d = %lu%s%s\t%s (default: %lu%s%s)%s", + t->T, t->val, + t->unit == OSMO_TDEF_CUSTOM ? "" : " ", t->unit == OSMO_TDEF_CUSTOM ? "" : osmo_tdef_unit_name(t->unit), + t->desc, t->default_val, + t->unit == OSMO_TDEF_CUSTOM ? "" : " ", t->unit == OSMO_TDEF_CUSTOM ? "" : osmo_tdef_unit_name(t->unit), + VTY_NEWLINE); +} + +/*! Write to VTY the current status of one timer. + * \param[in] vty VTY context for vty_out(). + * \param[in] t The timer to print. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_out_one(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, ...) +{ + va_list va; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va); +} + +/*! Write to VTY the current status of all given timers. + * \param[in] vty VTY context for vty_out(). + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable vprintf like arguments. + * \param[in] va va_list instance. As always, call va_start() before, and va_end() after this call. + */ +void osmo_tdef_vty_out_all_va(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, va_list va) +{ + struct osmo_tdef *t; + if (!tdefs) { + vty_out(vty, "%% Error: no such timers%s", VTY_NEWLINE); + return; + } + osmo_tdef_for_each(t, tdefs) { + va_list va2; + va_copy(va2, va); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va2); + } +} + +/*! Write to VTY the current status of all given timers. + * \param[in] vty VTY context for vty_out(). + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_out_all(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...) +{ + va_list va; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_all_va(vty, tdefs, prefix_fmt, va); + va_end(va); +} + +/*! Write current timer configuration arguments to the vty. Skip all entries that reflect their default value. + * The passed prefix string must contain both necessary indent and the VTY command the specific implementation is using. + * See tdef_vty_test_config_subnode.c and tdef_vty_test_dynamic.c for examples. + * \param[in] vty VTY context. + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_write(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...) +{ + va_list va; + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) { + if (t->val == t->default_val) + continue; + if (prefix_fmt && *prefix_fmt) { + va_start(va, prefix_fmt); + vty_out_va(vty, prefix_fmt, va); + va_end(va); + } + vty_out(vty, "T%d %lu%s", t->T, t->val, VTY_NEWLINE); + } +} + +/*! Singleton Tnnn groups definition as set by osmo_tdef_vty_groups_init(). */ +static struct osmo_tdef_group *global_tdef_groups; + +/*! \return true iff the first characters of str fully match startswith_str or both are empty. */ +static bool startswith(const char *str, const char *startswith_str) +{ + if (!startswith_str) + return true; + if (!str) + return false; + return strncmp(str, startswith_str, strlen(startswith_str)) == 0; +} + +DEFUN(show_timer, show_timer_cmd, "DYNAMIC", "DYNAMIC") + /* show timer [(alpha|beta|gamma)] [TNNNN] */ +{ + const char *group_arg = argc > 0 ? argv[0] : NULL; + const char *T_arg = argc > 1 ? argv[1] : NULL; + struct osmo_tdef_group *g; + + /* The argument should be either "tea" or "software", but the VTY also allows partial arguments + * like "softw" or "t" (which can also be ambiguous). */ + + osmo_tdef_groups_for_each(g, global_tdef_groups) { + if (!group_arg || startswith(g->name, group_arg)) + osmo_tdef_vty_show_cmd(vty, g->tdefs, T_arg, "%s: ", g->name); + } + return CMD_SUCCESS; +} + +DEFUN(cfg_timer, cfg_timer_cmd, "DYNAMIC", "DYNAMIC") + /* show timer [(alpha|beta|gamma)] [TNNNN] [(<0-2147483647>|default)] */ +{ + const char *group_arg; + const char **timer_args; + struct osmo_tdef *tdefs = NULL; + struct osmo_tdef_group *g = NULL; + + /* If any arguments are missing, redirect to 'show' */ + if (argc < 3) + return show_timer(self, vty, argc, argv); + + /* If all arguments are passed, this is configuring a timer. */ + group_arg = argc > 0 ? argv[0] : NULL; + timer_args = argv + 1; + osmo_tdef_groups_for_each(g, global_tdef_groups) { + if (strcmp(g->name, group_arg)) + continue; + if (tdefs) { + vty_out(vty, "%% Error: ambiguous timer group match%s", VTY_NEWLINE); + return CMD_WARNING; + } + tdefs = g->tdefs; + } + + return osmo_tdef_vty_set_cmd(vty, tdefs, timer_args); +} + +static char *add_group_args(void *talloc_ctx, char *dest) +{ + struct osmo_tdef_group *g; + osmo_talloc_asprintf(talloc_ctx, dest, "[("); + osmo_tdef_groups_for_each(g, global_tdef_groups) { + osmo_talloc_asprintf(talloc_ctx, dest, "%s%s", + (g == global_tdef_groups) ? "" : "|", + g->name); + } + osmo_talloc_asprintf(talloc_ctx, dest, ")]"); + return dest; +} + +static char *add_group_docs(void *talloc_ctx, char *dest) +{ + struct osmo_tdef_group *g; + osmo_tdef_groups_for_each(g, global_tdef_groups) { + osmo_talloc_asprintf(talloc_ctx, dest, "%s\n", g->desc); + } + return dest; +} + +static char *timer_command_string(const char *prefix, const char *suffix) +{ + char *dest = NULL; + osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, "%s ", prefix); + dest = add_group_args(tall_vty_cmd_ctx, dest); + osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, " %s", suffix); + return dest; +} + +static char *timer_doc_string(const char *prefix, const char *suffix) +{ + char *dest = NULL; + osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, "%s ", prefix); + dest = add_group_docs(tall_vty_cmd_ctx, dest); + osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, " %s", suffix); + return dest; +} + +/*! Convenience implementation for keeping a fixed set of timer groups in a program. + * Install a 'timer [(group|names|...)] [TNNN] [(|default)]' command under the given parent_node, + * and install a 'show timer...' command on VIEW_NODE and ENABLE_NODE. + * For a usage example, see \ref tdef_test_config_root.c. + * The given timer definitions group is stored in a global pointer, so this can be done only once per main() scope. + * It would also be possible to have distinct timer groups on separate VTY subnodes, with a "manual" implementation, but + * not with this API. + * \param[in] parent_node VTY node id at which to add the timer group commands, e.g. CONFIG_NODE. + * \param[in] groups Global timer groups definition. + */ +void osmo_tdef_vty_groups_init(enum node_type parent_node, struct osmo_tdef_group *groups) +{ + struct osmo_tdef_group *g; + OSMO_ASSERT(!global_tdef_groups); + global_tdef_groups = groups; + + osmo_tdef_groups_for_each(g, global_tdef_groups) + osmo_tdefs_reset(g->tdefs); + + show_timer_cmd.string = timer_command_string("show timer", OSMO_TDEF_VTY_ARG_T_OPTIONAL); + show_timer_cmd.doc = timer_doc_string(SHOW_STR "Show timers\n", OSMO_TDEF_VTY_DOC_T); + + cfg_timer_cmd.string = timer_command_string("timer", OSMO_TDEF_VTY_ARG_SET_OPTIONAL); + cfg_timer_cmd.doc = timer_doc_string("Configure or show timers\n", OSMO_TDEF_VTY_DOC_SET); + + install_element_ve(&show_timer_cmd); + install_element(parent_node, &cfg_timer_cmd); +} + +/*! Write the global osmo_tdef_group configuration to VTY, as previously passed to osmo_tdef_vty_groups_init(). + * \param[in] vty VTY context. + * \param[in] indent String to print before each line. + */ +void osmo_tdef_vty_groups_write(struct vty *vty, const char *indent) +{ + struct osmo_tdef_group *g; + osmo_tdef_groups_for_each(g, global_tdef_groups) + osmo_tdef_vty_write(vty, g->tdefs, "%stimer %s ", indent ? : "", g->name); +} + +/*! @} */ diff --git a/tests/Makefile.am b/tests/Makefile.am index 91f042e86..54fb11f08 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -27,6 +27,9 @@ check_PROGRAMS = timer/timer_test sms/sms_test ussd/ussd_test \ oap/oap_client_test gsm29205/gsm29205_test \ logging/logging_vty_test \ vty/vty_transcript_test \ + tdef/tdef_test tdef/tdef_vty_test_config_root \ + tdef/tdef_vty_test_config_subnode \ + tdef/tdef_vty_test_dynamic \ $(NULL) if ENABLE_MSGFILE @@ -221,6 +224,18 @@ prbs_prbs_test_SOURCES = prbs/prbs_test.c gsm23003_gsm23003_test_SOURCES = gsm23003/gsm23003_test.c gsm23003_gsm23003_test_LDADD = $(LDADD) $(top_builddir)/src/gsm/libosmogsm.la +tdef_tdef_test_SOURCES = tdef/tdef_test.c +tdef_tdef_test_LDADD = $(LDADD) + +tdef_tdef_vty_test_config_root_SOURCES = tdef/tdef_vty_test_config_root.c +tdef_tdef_vty_test_config_root_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la + +tdef_tdef_vty_test_config_subnode_SOURCES = tdef/tdef_vty_test_config_subnode.c +tdef_tdef_vty_test_config_subnode_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la + +tdef_tdef_vty_test_dynamic_SOURCES = tdef/tdef_vty_test_dynamic.c +tdef_tdef_vty_test_dynamic_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la + # The `:;' works around a Bash 3.2 bug when the output is not writeable. $(srcdir)/package.m4: $(top_srcdir)/configure.ac :;{ \ @@ -284,6 +299,10 @@ EXTRA_DIST = testsuite.at $(srcdir)/package.m4 $(TESTSUITE) \ timer/clk_override_test.ok \ oap/oap_client_test.ok oap/oap_client_test.err \ vty/vty_transcript_test.vty \ + tdef/tdef_test.ok \ + tdef/tdef_vty_test_config_root.vty \ + tdef/tdef_vty_test_config_subnode.vty \ + tdef/tdef_vty_test_dynamic.vty \ $(NULL) DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c @@ -328,6 +347,7 @@ endif # To update the VTY script from current application behavior, # pass -u to osmo_verify_transcript_vty.py by doing: # make vty-test U=-u + vty-test-logging: osmo_verify_transcript_vty.py -v \ -p 42042 \ @@ -340,9 +360,25 @@ vty-test-vty: -r "$(top_builddir)/tests/vty/vty_transcript_test" \ $(U) $(srcdir)/vty/*.vty +vty-test-tdef: + osmo_verify_transcript_vty.py -v \ + -p 42042 \ + -r "$(top_builddir)/tests/tdef/tdef_vty_test_config_root" \ + $(U) $(srcdir)/tdef/tdef_vty_test_config_root.vty + osmo_verify_transcript_vty.py -v \ + -p 42042 \ + -r "$(top_builddir)/tests/tdef/tdef_vty_test_config_subnode" \ + $(U) $(srcdir)/tdef/tdef_vty_test_config_subnode.vty + osmo_verify_transcript_vty.py -v \ + -p 42042 \ + -r "$(top_builddir)/tests/tdef/tdef_vty_test_dynamic" \ + $(U) $(srcdir)/tdef/tdef_vty_test_dynamic.vty + +# don't run vty tests concurrently so that the ports don't conflict vty-test: $(MAKE) vty-test-logging $(MAKE) vty-test-vty + $(MAKE) vty-test-tdef ctrl-test: echo "No CTRL tests exist currently" diff --git a/tests/tdef/tdef_test.c b/tests/tdef/tdef_test.c new file mode 100644 index 000000000..682c7ac79 --- /dev/null +++ b/tests/tdef/tdef_test.c @@ -0,0 +1,445 @@ +/* Test implementation for osmo_tdef API. */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include +#include +#include + +#include + +static void *ctx = NULL; + +static struct osmo_tdef tdefs[] = { + { .T=1, .default_val=100, .desc="100s" }, + { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="100ms" }, + { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="100m" }, + { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="100 potatoes" }, + + { .T=7, .default_val=50, .desc="Water Boiling Timeout" }, // default is .unit=OSMO_TDEF_S == 0 + { .T=8, .default_val=300, .desc="Tea brewing" }, + { .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" }, + { .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" }, + + /* test conversions */ + { .T=1000, .default_val=2*1000, .unit=OSMO_TDEF_MS, .desc="two seconds from ms" }, + { .T=1001, .default_val=60*1000, .unit=OSMO_TDEF_MS, .desc="one minute from ms" }, + { .T=1002, .default_val=(ULONG_MAX/60), .unit=OSMO_TDEF_M, .desc="almost too many seconds" }, + { .T=1003, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="too many seconds" }, + { .T=1004, .default_val=1, .unit=OSMO_TDEF_MS, .desc="one ms" }, + { .T=1005, .default_val=0, .unit=OSMO_TDEF_MS, .desc="zero ms" }, + { .T=1006, .default_val=0, .unit=OSMO_TDEF_S, .desc="zero s" }, + { .T=1007, .default_val=0, .unit=OSMO_TDEF_M, .desc="zero m" }, + { .T=1008, .default_val=0, .unit=OSMO_TDEF_CUSTOM, .desc="zero" }, + + /* test range */ + { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" }, + { .T=INT_MAX-1, .default_val=ULONG_MAX-1, .unit=OSMO_TDEF_S, .desc="very large" }, + { .T=INT_MAX-2, .default_val=LONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" }, + { .T=INT_MAX-3, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="very large in minutes" }, + { .T=INT_MIN, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="negative" }, + + { .T=0, .default_val=1, .unit=OSMO_TDEF_CUSTOM, .desc="zero" }, + + /* no desc */ + { .T=123, .default_val=1 }, + + {} // <-- important! last entry shall be zero +}; + +#define print_tdef_get(T, AS_UNIT) do { \ + unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \ + printf("osmo_tdef_get(tdefs, %d, %s, 999)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \ + } while (0) + +#define print_tdef_get_short(T, AS_UNIT) do { \ + unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \ + printf("osmo_tdef_get(%d, %s)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \ + } while (0) + +void print_tdef_info(unsigned int T) +{ + const struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, T); + if (!t) { + printf("T%d=NULL", T); + return; + } + printf("T%d=%lu%s", T, t->val, osmo_tdef_unit_name(t->unit)); + if (t->val != t->default_val) + printf("(def=%lu)", t->default_val); + printf("\n"); +} + +static void test_tdef_get() +{ + int i; + enum osmo_tdef_unit as_unit; + + printf("\n%s()\n", __func__); + + osmo_tdefs_reset(tdefs); // make all values the default + + for (i = 0; i < ARRAY_SIZE(tdefs)-1; i++) { + unsigned int T = tdefs[i].T; + print_tdef_info(T); + for (as_unit = OSMO_TDEF_S; as_unit <= OSMO_TDEF_CUSTOM; as_unit++) { + print_tdef_get_short(T, as_unit); + } + } +} + +static void test_tdef_get_nonexisting() +{ + printf("\n%s()\n", __func__); + + print_tdef_get(5, OSMO_TDEF_S); + print_tdef_get(5, OSMO_TDEF_MS); + print_tdef_get(5, OSMO_TDEF_M); + print_tdef_get(5, OSMO_TDEF_CUSTOM); +} + +static void test_tdef_set_and_get() +{ + struct osmo_tdef *t; + printf("\n%s()\n", __func__); + + t = osmo_tdef_get_entry(tdefs, 7); + printf("setting 7 = 42\n"); + t->val = 42; + print_tdef_info(7); + print_tdef_get_short(7, OSMO_TDEF_MS); + print_tdef_get_short(7, OSMO_TDEF_S); + print_tdef_get_short(7, OSMO_TDEF_M); + print_tdef_get_short(7, OSMO_TDEF_CUSTOM); + + printf("setting 7 = 420\n"); + t->val = 420; + print_tdef_info(7); + print_tdef_get_short(7, OSMO_TDEF_MS); + print_tdef_get_short(7, OSMO_TDEF_S); + print_tdef_get_short(7, OSMO_TDEF_M); + print_tdef_get_short(7, OSMO_TDEF_CUSTOM); + + printf("resetting\n"); + osmo_tdefs_reset(tdefs); + print_tdef_info(7); + print_tdef_get_short(7, OSMO_TDEF_S); +} + +enum test_tdef_fsm_states { + S_A = 0, + S_B, + S_C, + S_D, + S_G, + S_H, + S_I, + S_J, + S_K, + S_L, + S_M, + S_N, + S_O, + S_X, + S_Y, + S_Z, +}; + +static const struct osmo_tdef_state_timeout test_tdef_state_timeouts[32] = { + [S_A] = { .T = 1 }, + [S_B] = { .T = 2 }, + [S_C] = { .T = 3 }, + [S_D] = { .T = 4 }, + + [S_G] = { .T = 7 }, + [S_H] = { .T = 8 }, + [S_I] = { .T = 9 }, + [S_J] = { .T = 10 }, + + /* keep_timer: adopt whichever T was running before and continue the timeout. */ + [S_K] = { .keep_timer = true }, + /* S_F defines an undefined T, but should continue previous state's timeout. */ + [S_L] = { .T = 123, .keep_timer = true }, + + /* range */ + [S_M] = { .T = INT_MAX }, + [S_N] = { .T = INT_MIN }, + + /* T0 is not addressable from osmo_tdef_state_timeout, since it is indistinguishable from an unset entry. Even + * though a timeout value is set for T=0, the transition to state S_O will show "no timer configured". */ + [S_O] = { .T = 0 }, + + /* S_X undefined on purpose */ + /* S_Y defines a T that does not exist */ + [S_Y] = { .T = 666 }, + /* S_Z undefined on purpose */ +}; + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state test_tdef_fsm_states[] = { +#define DEF_STATE(NAME) \ + [S_##NAME] = { \ + .name = #NAME, \ + .out_state_mask = 0 \ + | S(S_A) \ + | S(S_B) \ + | S(S_C) \ + | S(S_D) \ + | S(S_G) \ + | S(S_H) \ + | S(S_I) \ + | S(S_J) \ + | S(S_K) \ + | S(S_L) \ + | S(S_M) \ + | S(S_N) \ + | S(S_O) \ + | S(S_X) \ + | S(S_Y) \ + | S(S_Z) \ + , \ + } + + DEF_STATE(A), + DEF_STATE(B), + DEF_STATE(C), + DEF_STATE(D), + + DEF_STATE(G), + DEF_STATE(H), + DEF_STATE(I), + DEF_STATE(J), + + DEF_STATE(K), + DEF_STATE(L), + + DEF_STATE(M), + DEF_STATE(N), + DEF_STATE(O), + + DEF_STATE(X), + DEF_STATE(Y), + /* Z: test not being allowed to transition to other states. */ + [S_Z] = { + .name = "Z", + .out_state_mask = 0 + | S(S_A) + , + }, +}; + +static const struct value_string test_tdef_fsm_event_names[] = { {} }; + +static struct osmo_fsm test_tdef_fsm = { + .name = "tdef_test", + .states = test_tdef_fsm_states, + .event_names = test_tdef_fsm_event_names, + .num_states = ARRAY_SIZE(test_tdef_fsm_states), + .log_subsys = DLGLOBAL, +}; + +const struct timeval fake_time_start_time = { 123, 456 }; + +#define fake_time_passes(secs, usecs) do \ +{ \ + struct timeval diff; \ + osmo_gettimeofday_override_add(secs, usecs); \ + osmo_clock_override_add(CLOCK_MONOTONIC, secs, usecs * 1000); \ + timersub(&osmo_gettimeofday_override_time, &fake_time_start_time, &diff); \ + printf("Total time passed: %ld.%06ld s\n", diff.tv_sec, diff.tv_usec); \ + osmo_timers_prepare(); \ + osmo_timers_update(); \ +} while (0) + +void fake_time_start() +{ + struct timespec *clock_override; + + osmo_gettimeofday_override_time = fake_time_start_time; + osmo_gettimeofday_override = true; + clock_override = osmo_clock_override_gettimespec(CLOCK_MONOTONIC); + OSMO_ASSERT(clock_override); + clock_override->tv_sec = fake_time_start_time.tv_sec; + clock_override->tv_nsec = fake_time_start_time.tv_usec * 1000; + osmo_clock_override_enable(CLOCK_MONOTONIC, true); + fake_time_passes(0, 0); +} + +static void print_fsm_state(struct osmo_fsm_inst *fi) +{ + struct timeval remaining; + printf("state=%s T=%d", osmo_fsm_inst_state_name(fi), fi->T); + + if (!osmo_timer_pending(&fi->timer)) { + printf(", no timeout\n"); + return; + } + + osmo_timer_remaining(&fi->timer, &osmo_gettimeofday_override_time, &remaining); + printf(", %lu.%06lu s remaining\n", remaining.tv_sec, remaining.tv_usec); +} + + +#define test_tdef_fsm_state_chg(NEXT_STATE) do { \ + const struct osmo_tdef_state_timeout *st = osmo_tdef_get_state_timeout(NEXT_STATE, \ + test_tdef_state_timeouts); \ + if (!st) { \ + printf(" --> %s (no timer configured for this state)\n", \ + osmo_fsm_state_name(&test_tdef_fsm, NEXT_STATE)); \ + } else { \ + struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, st->T); \ + int rc = osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, test_tdef_state_timeouts, tdefs, 999); \ + printf(" --> %s (configured as T%d%s %lu %s) rc=%d;\t", osmo_fsm_state_name(&test_tdef_fsm, \ + NEXT_STATE), \ + st->T, st->keep_timer ? "(keep_timer)" : "", \ + t? t->val : -1, t? osmo_tdef_unit_name(t->unit) : "-", \ + rc); \ + print_fsm_state(fi); \ + } \ + } while(0) + + + +static void test_tdef_state_timeout(bool test_range) +{ + struct osmo_fsm_inst *fi; + struct osmo_tdef *m = osmo_tdef_get_entry(tdefs, INT_MAX); + unsigned long m_secs; + printf("\n%s()\n", __func__); + + osmo_tdefs_reset(tdefs); + + fake_time_start(); + + fi = osmo_fsm_inst_alloc(&test_tdef_fsm, ctx, NULL, LOGL_DEBUG, __func__); + OSMO_ASSERT(fi); + print_fsm_state(fi); + + test_tdef_fsm_state_chg(S_A); + test_tdef_fsm_state_chg(S_B); + test_tdef_fsm_state_chg(S_C); + test_tdef_fsm_state_chg(S_D); + + test_tdef_fsm_state_chg(S_G); + test_tdef_fsm_state_chg(S_H); + test_tdef_fsm_state_chg(S_I); + test_tdef_fsm_state_chg(S_J); + + printf("- test keep_timer:\n"); + fake_time_passes(123, 45678); + print_fsm_state(fi); + test_tdef_fsm_state_chg(S_K); + test_tdef_fsm_state_chg(S_A); + fake_time_passes(23, 45678); + print_fsm_state(fi); + test_tdef_fsm_state_chg(S_K); + + test_tdef_fsm_state_chg(S_A); + fake_time_passes(23, 45678); + print_fsm_state(fi); + test_tdef_fsm_state_chg(S_L); + + printf("- test large T:\n"); + test_tdef_fsm_state_chg(S_M); + + printf("- test T<0:\n"); + test_tdef_fsm_state_chg(S_N); + + printf("- test T=0:\n"); + test_tdef_fsm_state_chg(S_O); + + printf("- test no timer:\n"); + test_tdef_fsm_state_chg(S_X); + + printf("- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:\n"); + test_tdef_fsm_state_chg(S_Y); + + /* the range of unsigned long is architecture dependent. This test can be invoked manually to see whether + * clamping the timeout values works, but the output will be of varying lengths depending on the system's + * unsigned long range, and would cause differences in expected output. */ + if (test_range) { + printf("- test range:\n"); + test_tdef_fsm_state_chg(S_M); + /* sweep through all the bits, shifting in 0xfffff.. from the right. */ + m_secs = 0; + do { + m_secs = (m_secs << 1) + 1; + switch (m_secs) { + case 0x7fff: + printf("--- int32_t max ---\n"); + break; + case 0xffff: + printf("--- uint32_t max ---\n"); + break; + case 0x7fffffff: + printf("--- int64_t max ---\n"); + break; + case 0xffffffff: + printf("--- uint64_t max ---\n"); + break; + default: + break; + } + + m->val = m_secs - 1; + test_tdef_fsm_state_chg(S_M); + m->val = m_secs; + test_tdef_fsm_state_chg(S_M); + m->val = m_secs + 1; + test_tdef_fsm_state_chg(S_M); + } while (m_secs < ULONG_MAX); + } + + printf("- test disallowed transition:\n"); + test_tdef_fsm_state_chg(S_Z); + test_tdef_fsm_state_chg(S_B); + test_tdef_fsm_state_chg(S_C); + test_tdef_fsm_state_chg(S_D); +} + +int main(int argc, char **argv) +{ + ctx = talloc_named_const(NULL, 0, "tdef_test.c"); + osmo_init_logging2(ctx, NULL); + + log_set_print_filename(osmo_stderr_target, 0); + log_set_print_category(osmo_stderr_target, 1); + log_set_use_color(osmo_stderr_target, 0); + + osmo_fsm_register(&test_tdef_fsm); + + test_tdef_get(); + test_tdef_get_nonexisting(); + test_tdef_set_and_get(); + /* Run range test iff any argument is passed on the cmdline. For the rationale, see the comment in + * test_tdef_state_timeout(). */ + test_tdef_state_timeout(argc > 1); + + return EXIT_SUCCESS; +} diff --git a/tests/tdef/tdef_test.ok b/tests/tdef/tdef_test.ok new file mode 100644 index 000000000..cf4b77f54 --- /dev/null +++ b/tests/tdef/tdef_test.ok @@ -0,0 +1,184 @@ + +test_tdef_get() +T1=100s +osmo_tdef_get(1, s) = 100 +osmo_tdef_get(1, ms) = 100000 +osmo_tdef_get(1, m) = 2 +osmo_tdef_get(1, custom-unit) = 100 +T2=100ms +osmo_tdef_get(2, s) = 1 +osmo_tdef_get(2, ms) = 100 +osmo_tdef_get(2, m) = 1 +osmo_tdef_get(2, custom-unit) = 100 +T3=100m +osmo_tdef_get(3, s) = 6000 +osmo_tdef_get(3, ms) = 6000000 +osmo_tdef_get(3, m) = 100 +osmo_tdef_get(3, custom-unit) = 100 +T4=100custom-unit +osmo_tdef_get(4, s) = 100 +osmo_tdef_get(4, ms) = 100 +osmo_tdef_get(4, m) = 100 +osmo_tdef_get(4, custom-unit) = 100 +T7=50s +osmo_tdef_get(7, s) = 50 +osmo_tdef_get(7, ms) = 50000 +osmo_tdef_get(7, m) = 1 +osmo_tdef_get(7, custom-unit) = 50 +T8=300s +osmo_tdef_get(8, s) = 300 +osmo_tdef_get(8, ms) = 300000 +osmo_tdef_get(8, m) = 5 +osmo_tdef_get(8, custom-unit) = 300 +T9=5m +osmo_tdef_get(9, s) = 300 +osmo_tdef_get(9, ms) = 300000 +osmo_tdef_get(9, m) = 5 +osmo_tdef_get(9, custom-unit) = 5 +T10=20m +osmo_tdef_get(10, s) = 1200 +osmo_tdef_get(10, ms) = 1200000 +osmo_tdef_get(10, m) = 20 +osmo_tdef_get(10, custom-unit) = 20 +T1000=2000ms +osmo_tdef_get(1000, s) = 2 +osmo_tdef_get(1000, ms) = 2000 +osmo_tdef_get(1000, m) = 1 +osmo_tdef_get(1000, custom-unit) = 2000 +T1001=60000ms +osmo_tdef_get(1001, s) = 60 +osmo_tdef_get(1001, ms) = 60000 +osmo_tdef_get(1001, m) = 1 +osmo_tdef_get(1001, custom-unit) = 60000 +T1002=307445734561825860m +osmo_tdef_get(1002, s) = 18446744073709551600 +osmo_tdef_get(1002, ms) = 18446744073709551615 +osmo_tdef_get(1002, m) = 307445734561825860 +osmo_tdef_get(1002, custom-unit) = 307445734561825860 +T1003=18446744073709551615m +osmo_tdef_get(1003, s) = 18446744073709551615 +osmo_tdef_get(1003, ms) = 18446744073709551615 +osmo_tdef_get(1003, m) = 18446744073709551615 +osmo_tdef_get(1003, custom-unit) = 18446744073709551615 +T1004=1ms +osmo_tdef_get(1004, s) = 1 +osmo_tdef_get(1004, ms) = 1 +osmo_tdef_get(1004, m) = 1 +osmo_tdef_get(1004, custom-unit) = 1 +T1005=0ms +osmo_tdef_get(1005, s) = 0 +osmo_tdef_get(1005, ms) = 0 +osmo_tdef_get(1005, m) = 0 +osmo_tdef_get(1005, custom-unit) = 0 +T1006=0s +osmo_tdef_get(1006, s) = 0 +osmo_tdef_get(1006, ms) = 0 +osmo_tdef_get(1006, m) = 0 +osmo_tdef_get(1006, custom-unit) = 0 +T1007=0m +osmo_tdef_get(1007, s) = 0 +osmo_tdef_get(1007, ms) = 0 +osmo_tdef_get(1007, m) = 0 +osmo_tdef_get(1007, custom-unit) = 0 +T1008=0custom-unit +osmo_tdef_get(1008, s) = 0 +osmo_tdef_get(1008, ms) = 0 +osmo_tdef_get(1008, m) = 0 +osmo_tdef_get(1008, custom-unit) = 0 +T2147483647=18446744073709551615s +osmo_tdef_get(2147483647, s) = 18446744073709551615 +osmo_tdef_get(2147483647, ms) = 18446744073709551615 +osmo_tdef_get(2147483647, m) = 307445734561825861 +osmo_tdef_get(2147483647, custom-unit) = 18446744073709551615 +T2147483646=18446744073709551614s +osmo_tdef_get(2147483646, s) = 18446744073709551614 +osmo_tdef_get(2147483646, ms) = 18446744073709551615 +osmo_tdef_get(2147483646, m) = 307445734561825861 +osmo_tdef_get(2147483646, custom-unit) = 18446744073709551614 +T2147483645=9223372036854775807s +osmo_tdef_get(2147483645, s) = 9223372036854775807 +osmo_tdef_get(2147483645, ms) = 18446744073709551615 +osmo_tdef_get(2147483645, m) = 153722867280912931 +osmo_tdef_get(2147483645, custom-unit) = 9223372036854775807 +T2147483644=18446744073709551615m +osmo_tdef_get(2147483644, s) = 18446744073709551615 +osmo_tdef_get(2147483644, ms) = 18446744073709551615 +osmo_tdef_get(2147483644, m) = 18446744073709551615 +osmo_tdef_get(2147483644, custom-unit) = 18446744073709551615 +T-2147483648=18446744073709551615s +osmo_tdef_get(-2147483648, s) = 18446744073709551615 +osmo_tdef_get(-2147483648, ms) = 18446744073709551615 +osmo_tdef_get(-2147483648, m) = 307445734561825861 +osmo_tdef_get(-2147483648, custom-unit) = 18446744073709551615 +T0=1custom-unit +osmo_tdef_get(0, s) = 1 +osmo_tdef_get(0, ms) = 1 +osmo_tdef_get(0, m) = 1 +osmo_tdef_get(0, custom-unit) = 1 +T123=1s +osmo_tdef_get(123, s) = 1 +osmo_tdef_get(123, ms) = 1000 +osmo_tdef_get(123, m) = 1 +osmo_tdef_get(123, custom-unit) = 1 + +test_tdef_get_nonexisting() +osmo_tdef_get(tdefs, 5, s, 999) = 999 +osmo_tdef_get(tdefs, 5, ms, 999) = 999 +osmo_tdef_get(tdefs, 5, m, 999) = 999 +osmo_tdef_get(tdefs, 5, custom-unit, 999) = 999 + +test_tdef_set_and_get() +setting 7 = 42 +T7=42s(def=50) +osmo_tdef_get(7, ms) = 42000 +osmo_tdef_get(7, s) = 42 +osmo_tdef_get(7, m) = 1 +osmo_tdef_get(7, custom-unit) = 42 +setting 7 = 420 +T7=420s(def=50) +osmo_tdef_get(7, ms) = 420000 +osmo_tdef_get(7, s) = 420 +osmo_tdef_get(7, m) = 7 +osmo_tdef_get(7, custom-unit) = 420 +resetting +T7=50s +osmo_tdef_get(7, s) = 50 + +test_tdef_state_timeout() +Total time passed: 0.000000 s +state=A T=0, no timeout + --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining + --> B (configured as T2 100 ms) rc=0; state=B T=2, 1.000000 s remaining + --> C (configured as T3 100 m) rc=0; state=C T=3, 6000.000000 s remaining + --> D (configured as T4 100 custom-unit) rc=0; state=D T=4, 100.000000 s remaining + --> G (configured as T7 50 s) rc=0; state=G T=7, 50.000000 s remaining + --> H (configured as T8 300 s) rc=0; state=H T=8, 300.000000 s remaining + --> I (configured as T9 5 m) rc=0; state=I T=9, 300.000000 s remaining + --> J (configured as T10 20 m) rc=0; state=J T=10, 1200.000000 s remaining +- test keep_timer: +Total time passed: 123.045678 s +state=J T=10, 1076.954322 s remaining + --> K (configured as T0(keep_timer) 1 custom-unit) rc=0; state=K T=10, 1076.954322 s remaining + --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining +Total time passed: 146.091356 s +state=A T=1, 76.954322 s remaining + --> K (configured as T0(keep_timer) 1 custom-unit) rc=0; state=K T=1, 76.954322 s remaining + --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining +Total time passed: 169.137034 s +state=A T=1, 76.954322 s remaining + --> L (configured as T123(keep_timer) 1 s) rc=0; state=L T=123, 76.954322 s remaining +- test large T: + --> M (configured as T2147483647 18446744073709551615 s) rc=0; state=M T=2147483647, 2147483647.000000 s remaining +- test T<0: + --> N (configured as T-2147483648 18446744073709551615 s) rc=0; state=N T=-2147483648, 2147483647.000000 s remaining +- test T=0: + --> O (no timer configured for this state) +- test no timer: + --> X (no timer configured for this state) +- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999: + --> Y (configured as T666 18446744073709551615 -) rc=0; state=Y T=666, 999.000000 s remaining +- test disallowed transition: + --> Z (no timer configured for this state) + --> B (configured as T2 100 ms) rc=0; state=B T=2, 1.000000 s remaining + --> C (configured as T3 100 m) rc=0; state=C T=3, 6000.000000 s remaining + --> D (configured as T4 100 custom-unit) rc=0; state=D T=4, 100.000000 s remaining diff --git a/tests/tdef/tdef_vty_test_config_root.c b/tests/tdef/tdef_vty_test_config_root.c new file mode 100644 index 000000000..138ac00a5 --- /dev/null +++ b/tests/tdef/tdef_vty_test_config_root.c @@ -0,0 +1,292 @@ +/* Test implementation for osmo_tdef VTY configuration API. */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include "config.h" + +/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */ + +/* This example keeps several separate timer groups and offers 'timer' VTY commands at the root of the config node. See + * the tdef_vty_test_config_root.vty transcript test. + */ + +static struct osmo_tdef tdefs_test[] = { + { .T=1, .default_val=100, .desc="Testing a hundred seconds" }, // default is .unit=OSMO_TDEF_S == 0 + { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="Testing a hundred milliseconds" }, + { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="Testing a hundred minutes" }, + { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="Testing a hundred potatoes" }, + { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="Very large" }, + { .T=-23, .default_val=-15, .desc="Negative T number" }, + {} // <-- important! last entry shall be zero +}; + +static struct osmo_tdef tdefs_tea[] = { + { .T=1, .default_val=50, .desc="Water Boiling Timeout" }, + { .T=2, .default_val=300, .desc="Tea brewing" }, + { .T=3, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" }, + { .T=4, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" }, + {} +}; + +static struct osmo_tdef tdefs_software[] = { + { .T=1, .default_val=30, .unit=OSMO_TDEF_M, .desc="Write code" }, + { .T=2, .default_val=20, .unit=OSMO_TDEF_MS, .desc="Hit segfault" }, + { .T=3, .default_val=480, .unit=OSMO_TDEF_M, .desc="Fix bugs" }, + {} +}; + +static struct osmo_tdef_group tdef_groups[] = { + { + .name = "tea", + .desc = "Tea time", + .tdefs = tdefs_tea, + }, + { + .name = "test", + .desc = "Test timers", + .tdefs = tdefs_test, + }, + { + .name = "software", + .desc = "Typical software development cycle", + .tdefs = tdefs_software, + }, + {} +}; + +enum tdef_vty_test_nodes { + TIMER_NODE = _LAST_OSMOVTY_NODE + 1, +}; + +/* This example puts 'timer' configuration commands directly at the root of the CONFIG_NODE. + * This TIMER_NODE is merely needed as a hook for the vty_write() command, but becomes an empty node in the VTY docs. + * It is possible to cheat around needing this if you choose to config_write_timer() in another root nodes' write cb. + * Another example using a 'network' subnode is \ref tdef_vty_test_config_subnode.c */ +static struct cmd_node timer_node = { + TIMER_NODE, + "%s(config-timer)# ", + 1, +}; + +static int config_write_timer(struct vty *vty) +{ + osmo_tdef_vty_groups_write(vty, ""); + return CMD_SUCCESS; +} + +static void timer_init_vty() +{ + /* Again, this is merely to get a vty write hook, see above. */ + install_node(&timer_node, config_write_timer); + + osmo_tdef_vty_groups_init(CONFIG_NODE, tdef_groups); +} + +/* ------------------- THE REST is just boilerplate osmo main() ------------------- */ + +void *root_ctx = NULL; + +static void print_help() +{ + printf( "options:\n" + " -h --help this text\n" + " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n" + " -D --daemonize For the process into a background daemon\n" + " -c --config-file Specify the filename of the config file\n" + " -s --disable-color Don't use colors in stderr log output\n" + " -T --timestamp Prefix every log line with a timestamp\n" + " -V --version Print version information and exit\n" + " -e --log-level Set a global log-level\n" + ); +} + +static struct { + const char *config_file; + int daemonize; +} cmdline_config = {}; + +static void handle_options(int argc, char **argv) +{ + while (1) { + int option_idx = 0, c; + static const struct option long_options[] = { + { "help", 0, 0, 'h' }, + { "debug", 1, 0, 'd' }, + { "daemonize", 0, 0, 'D' }, + { "config-file", 1, 0, 'c' }, + { "disable-color", 0, 0, 's' }, + { "timestamp", 0, 0, 'T' }, + { "version", 0, 0, 'V' }, + { "log-level", 1, 0, 'e' }, + {} + }; + + c = getopt_long(argc, argv, "hc:d:Dc:sTVe:", + long_options, &option_idx); + if (c == -1) + break; + + switch (c) { + case 'h': + print_help(); + exit(0); + case 's': + log_set_use_color(osmo_stderr_target, 0); + break; + case 'd': + log_parse_category_mask(osmo_stderr_target, optarg); + break; + case 'D': + cmdline_config.daemonize = 1; + break; + case 'c': + cmdline_config.config_file = optarg; + break; + case 'T': + log_set_print_timestamp(osmo_stderr_target, 1); + break; + case 'e': + log_set_log_level(osmo_stderr_target, atoi(optarg)); + break; + case 'V': + print_version(1); + exit(0); + break; + default: + /* catch unknown options *as well as* missing arguments. */ + fprintf(stderr, "Error in command line options. Exiting.\n"); + exit(-1); + } + } +} + +static int quit = 0; + +static void signal_handler(int signal) +{ + fprintf(stdout, "signal %u received\n", signal); + + switch (signal) { + case SIGINT: + case SIGTERM: + quit++; + break; + case SIGABRT: + osmo_generate_backtrace(); + /* in case of abort, we want to obtain a talloc report + * and then return to the caller, who will abort the process */ + case SIGUSR1: + talloc_report(tall_vty_ctx, stderr); + talloc_report_full(root_ctx, stderr); + break; + case SIGUSR2: + talloc_report_full(tall_vty_ctx, stderr); + break; + default: + break; + } +} + +static struct vty_app_info vty_info = { + .name = "tdef_vty_test", + .version = PACKAGE_VERSION, +}; + +static const struct log_info_cat default_categories[] = {}; + +const struct log_info log_info = { + .cat = default_categories, + .num_cat = ARRAY_SIZE(default_categories), +}; + +int main(int argc, char **argv) +{ + int rc; + + root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test"); + + osmo_init_logging2(root_ctx, &log_info); + + vty_info.tall_ctx = root_ctx; + vty_init(&vty_info); + osmo_talloc_vty_add_cmds(); + + timer_init_vty(); /* <---- the only tdef relevant init */ + + handle_options(argc, argv); + + if (cmdline_config.config_file) { + rc = vty_read_config_file(cmdline_config.config_file, NULL); + if (rc < 0) { + fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file); + return 1; + } + } + + rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042); + if (rc < 0) + return 2; + + signal(SIGINT, &signal_handler); + signal(SIGTERM, &signal_handler); + signal(SIGABRT, &signal_handler); + signal(SIGUSR1, &signal_handler); + signal(SIGUSR2, &signal_handler); + osmo_init_ignore_signals(); + + if (cmdline_config.daemonize) { + rc = osmo_daemonize(); + if (rc < 0) { + perror("Error during daemonize"); + return 6; + } + } + + while (!quit) { + log_reset_context(); + osmo_select_main(0); + } + + talloc_free(root_ctx); + talloc_free(tall_vty_ctx); + + return 0; +} diff --git a/tests/tdef/tdef_vty_test_config_root.vty b/tests/tdef/tdef_vty_test_config_root.vty new file mode 100644 index 000000000..12876a685 --- /dev/null +++ b/tests/tdef/tdef_vty_test_config_root.vty @@ -0,0 +1,292 @@ +tdef_vty_test> list +... + show timer [(tea|test|software)] [TNNNN] +... + +tdef_vty_test> show timer ? + [tea] Tea time + [test] Test timers + [software] Typical software development cycle + +tdef_vty_test> show timer test ? + [TNNNN] T-number, optionally preceded by 't' or 'T'. + +tdef_vty_test> show timer +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) +software: T1 = 30 m Write code (default: 30 m) +software: T2 = 20 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test> enable +tdef_vty_test# show timer +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) +software: T1 = 30 m Write code (default: 30 m) +software: T2 = 20 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test# configure terminal + +tdef_vty_test(config)# show running-config +... !timer + +tdef_vty_test(config)# list +... + timer [(tea|test|software)] [TNNNN] [(<0-2147483647>|default)] +... + +tdef_vty_test(config)# timer ? + [tea] Tea time + [test] Test timers + [software] Typical software development cycle + +tdef_vty_test(config)# timer sof T123 ? + [<0-2147483647>] New timer value + [default] Set to default timer value + +tdef_vty_test(config)# timer sof T123 ? + [<0-2147483647>] New timer value + [default] Set to default timer value + +tdef_vty_test(config)# timer test ? + [TNNNN] T-number, optionally preceded by 't' or 'T'. + +tdef_vty_test(config)# timer test t2 ? + [<0-2147483647>] New timer value + [default] Set to default timer value + +tdef_vty_test(config)# do show timer +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) +software: T1 = 30 m Write code (default: 30 m) +software: T2 = 20 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# do show timer tea +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) + +tdef_vty_test(config)# do show timer tea 2 +tea: T2 = 300 s Tea brewing (default: 300 s) + +tdef_vty_test(config)# do show timer tea t2 +tea: T2 = 300 s Tea brewing (default: 300 s) + +tdef_vty_test(config)# do show timer tea T2 +tea: T2 = 300 s Tea brewing (default: 300 s) + +tdef_vty_test(config)# do show timer tea T5 +% No such timer: T5 + +tdef_vty_test(config)# do show timer tea T0 +% No such timer: T0 + +tdef_vty_test(config)# do show timer tea T-123 +% No such timer: T-123 + +tdef_vty_test(config)# do show timer t +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config)# do show timer te +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 5 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config)# do show timer te T2 +tea: T2 = 300 s Tea brewing (default: 300 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) + + +tdef_vty_test(config)# timer tea 3 30 +tdef_vty_test(config)# timer tea T3 +tea: T3 = 30 m Let tea cool down before drinking (default: 5 m) + +tdef_vty_test(config)# timer tea t3 31 +tdef_vty_test(config)# timer tea T3 +tea: T3 = 31 m Let tea cool down before drinking (default: 5 m) + +tdef_vty_test(config)# timer tea T3 32 +tdef_vty_test(config)# timer tea T3 +tea: T3 = 32 m Let tea cool down before drinking (default: 5 m) + +tdef_vty_test(config)# timer tea T-123 99 +% No such timer: T-123 + +tdef_vty_test(config)# timer tea T0 0 +% No such timer: T0 + +tdef_vty_test(config)# timer tea T123 default +% No such timer: T123 + +tdef_vty_test(config)# timer tea +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 32 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) + +tdef_vty_test(config)# timer t +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 32 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config)# timer te T2 +tea: T2 = 300 s Tea brewing (default: 300 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) + +tdef_vty_test(config)# timer test T2 100 + +tdef_vty_test(config)# timer tes T2 100 +% Error: no timers found + +tdef_vty_test(config)# timer te T2 100 +% Error: no timers found + + +tdef_vty_test(config)# do show timer software +software: T1 = 30 m Write code (default: 30 m) +software: T2 = 20 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# do show timer software 1 +software: T1 = 30 m Write code (default: 30 m) + +tdef_vty_test(config)# do show timer software t1 +software: T1 = 30 m Write code (default: 30 m) + +tdef_vty_test(config)# do show timer software T1 +software: T1 = 30 m Write code (default: 30 m) + +tdef_vty_test(config)# do show timer software T99 +% No such timer: T99 + +tdef_vty_test(config)# do show timer software T-123123 +% No such timer: T-123123 + +tdef_vty_test(config)# do show timer software T0 +% No such timer: T0 + +tdef_vty_test(config)# timer software 1 11 +tdef_vty_test(config)# timer software T1 +software: T1 = 11 m Write code (default: 30 m) + +tdef_vty_test(config)# timer software t1 12 +tdef_vty_test(config)# timer software T1 +software: T1 = 12 m Write code (default: 30 m) + +tdef_vty_test(config)# timer software T1 13 +tdef_vty_test(config)# timer software T2 0 +tdef_vty_test(config)# timer software +software: T1 = 13 m Write code (default: 30 m) +software: T2 = 0 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# timer softw +software: T1 = 13 m Write code (default: 30 m) +software: T2 = 0 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# timer softw T3 +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# timer softw T3 23 +% Error: no timers found + +tdef_vty_test(config)# timer +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 32 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) +software: T1 = 13 m Write code (default: 30 m) +software: T2 = 0 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# do show timer +tea: T1 = 50 s Water Boiling Timeout (default: 50 s) +tea: T2 = 300 s Tea brewing (default: 300 s) +tea: T3 = 32 m Let tea cool down before drinking (default: 5 m) +tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m) +test: T1 = 100 s Testing a hundred seconds (default: 100 s) +test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +test: T3 = 100 m Testing a hundred minutes (default: 100 m) +test: T4 = 100 Testing a hundred potatoes (default: 100) +test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) +software: T1 = 13 m Write code (default: 30 m) +software: T2 = 0 ms Hit segfault (default: 20 ms) +software: T3 = 480 m Fix bugs (default: 480 m) + +tdef_vty_test(config)# show running-config +... !timer +timer tea T3 32 +timer software T1 13 +timer software T2 0 +... !timer + +tdef_vty_test(config)# timer tea T3 default +tdef_vty_test(config)# timer software T1 default +tdef_vty_test(config)# show running-config +... !timer +timer software T2 0 +... !timer + +tdef_vty_test(config)# timer softw 2 default +% Error: no timers found +tdef_vty_test(config)# timer software 2 default +tdef_vty_test(config)# show running-config +... !timer diff --git a/tests/tdef/tdef_vty_test_config_subnode.c b/tests/tdef/tdef_vty_test_config_subnode.c new file mode 100644 index 000000000..c371c8d6d --- /dev/null +++ b/tests/tdef/tdef_vty_test_config_subnode.c @@ -0,0 +1,288 @@ +/* Test implementation for osmo_tdef VTY configuration API. */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include "config.h" + +/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */ + +/* This example keeps a single global timer group and offers a custom 'timer' VTY command in a 'network' subnode below + * the CONFIG_NODE. + * the tdef_vty_test_config_subnode.vty transcript test. + */ + +static struct osmo_tdef global_tdefs[] = { + { .T=1, .default_val=100, .desc="Testing a hundred seconds" }, // default is .unit=OSMO_TDEF_S == 0 + { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="Testing a hundred milliseconds" }, + { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="Testing a hundred minutes" }, + { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="Testing a hundred potatoes" }, + { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="Very large" }, + { .T=-23, .default_val=-15, .desc="Negative T number" }, + {} // <-- important! last entry shall be zero +}; + +enum tdef_vty_test_nodes { + GSMNET_NODE = _LAST_OSMOVTY_NODE + 1, +}; + +/* This example offers 'timer T123' commands within an "unrelated" already existing subnode. */ +static struct cmd_node gsmnet_node = { + GSMNET_NODE, + "%s(config-net)# ", + 1, +}; + +DEFUN(show_timer, show_timer_cmd, + "show timer " OSMO_TDEF_VTY_ARG_T_OPTIONAL, + SHOW_STR "Show timers\n" + OSMO_TDEF_VTY_DOC_T) +{ + const char *T_arg = argc > 0 ? argv[0] : NULL; + return osmo_tdef_vty_show_cmd(vty, global_tdefs, T_arg, NULL); +} + +DEFUN(cfg_net_timer, cfg_net_timer_cmd, + "timer " OSMO_TDEF_VTY_ARG_SET_OPTIONAL, + "Configure or show timers\n" + OSMO_TDEF_VTY_DOC_SET) +{ + /* If any arguments are missing, redirect to 'show' */ + if (argc < 2) + return show_timer(self, vty, argc, argv); + return osmo_tdef_vty_set_cmd(vty, global_tdefs, argv); +} + +DEFUN(cfg_net, cfg_net_cmd, + "network", "Enter network node\n") +{ + vty->node = GSMNET_NODE; + return CMD_SUCCESS; +} + +static int config_write_gsmnet(struct vty *vty) +{ + vty_out(vty, "net%s", VTY_NEWLINE); + /* usually, here would be the output of any other 'net' config items... */ + + osmo_tdef_vty_write(vty, global_tdefs, " timer "); + return CMD_SUCCESS; +} + +static void gsmnet_init_vty() +{ + install_node(&gsmnet_node, config_write_gsmnet); + install_element(CONFIG_NODE, &cfg_net_cmd); + + osmo_tdefs_reset(global_tdefs); + install_element_ve(&show_timer_cmd); + install_element(GSMNET_NODE, &cfg_net_timer_cmd); +} + +/* ------------------- THE REST is just boilerplate osmo main() ------------------- */ + +void *root_ctx = NULL; + +static void print_help() +{ + printf( "options:\n" + " -h --help this text\n" + " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n" + " -D --daemonize For the process into a background daemon\n" + " -c --config-file Specify the filename of the config file\n" + " -s --disable-color Don't use colors in stderr log output\n" + " -T --timestamp Prefix every log line with a timestamp\n" + " -V --version Print version information and exit\n" + " -e --log-level Set a global log-level\n" + ); +} + +static struct { + const char *config_file; + int daemonize; +} cmdline_config = {}; + +static void handle_options(int argc, char **argv) +{ + while (1) { + int option_idx = 0, c; + static const struct option long_options[] = { + { "help", 0, 0, 'h' }, + { "debug", 1, 0, 'd' }, + { "daemonize", 0, 0, 'D' }, + { "config-file", 1, 0, 'c' }, + { "disable-color", 0, 0, 's' }, + { "timestamp", 0, 0, 'T' }, + { "version", 0, 0, 'V' }, + { "log-level", 1, 0, 'e' }, + {} + }; + + c = getopt_long(argc, argv, "hc:d:Dc:sTVe:", + long_options, &option_idx); + if (c == -1) + break; + + switch (c) { + case 'h': + print_help(); + exit(0); + case 's': + log_set_use_color(osmo_stderr_target, 0); + break; + case 'd': + log_parse_category_mask(osmo_stderr_target, optarg); + break; + case 'D': + cmdline_config.daemonize = 1; + break; + case 'c': + cmdline_config.config_file = optarg; + break; + case 'T': + log_set_print_timestamp(osmo_stderr_target, 1); + break; + case 'e': + log_set_log_level(osmo_stderr_target, atoi(optarg)); + break; + case 'V': + print_version(1); + exit(0); + break; + default: + /* catch unknown options *as well as* missing arguments. */ + fprintf(stderr, "Error in command line options. Exiting.\n"); + exit(-1); + } + } +} + +static int quit = 0; + +static void signal_handler(int signal) +{ + fprintf(stdout, "signal %u received\n", signal); + + switch (signal) { + case SIGINT: + case SIGTERM: + quit++; + break; + case SIGABRT: + osmo_generate_backtrace(); + /* in case of abort, we want to obtain a talloc report + * and then return to the caller, who will abort the process */ + case SIGUSR1: + talloc_report(tall_vty_ctx, stderr); + talloc_report_full(root_ctx, stderr); + break; + case SIGUSR2: + talloc_report_full(tall_vty_ctx, stderr); + break; + default: + break; + } +} + +static struct vty_app_info vty_info = { + .name = "tdef_vty_test", + .version = PACKAGE_VERSION, +}; + +static const struct log_info_cat default_categories[] = {}; + +const struct log_info log_info = { + .cat = default_categories, + .num_cat = ARRAY_SIZE(default_categories), +}; + +int main(int argc, char **argv) +{ + int rc; + + root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test"); + + osmo_init_logging2(root_ctx, &log_info); + + vty_info.tall_ctx = root_ctx; + vty_init(&vty_info); + osmo_talloc_vty_add_cmds(); + + gsmnet_init_vty(); /* <--- relevant init for this example */ + + handle_options(argc, argv); + + if (cmdline_config.config_file) { + rc = vty_read_config_file(cmdline_config.config_file, NULL); + if (rc < 0) { + fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file); + return 1; + } + } + + rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042); + if (rc < 0) + return 2; + + signal(SIGINT, &signal_handler); + signal(SIGTERM, &signal_handler); + signal(SIGABRT, &signal_handler); + signal(SIGUSR1, &signal_handler); + signal(SIGUSR2, &signal_handler); + osmo_init_ignore_signals(); + + if (cmdline_config.daemonize) { + rc = osmo_daemonize(); + if (rc < 0) { + perror("Error during daemonize"); + return 6; + } + } + + while (!quit) { + log_reset_context(); + osmo_select_main(0); + } + + talloc_free(root_ctx); + talloc_free(tall_vty_ctx); + + return 0; +} diff --git a/tests/tdef/tdef_vty_test_config_subnode.vty b/tests/tdef/tdef_vty_test_config_subnode.vty new file mode 100644 index 000000000..6cfd3bf39 --- /dev/null +++ b/tests/tdef/tdef_vty_test_config_subnode.vty @@ -0,0 +1,107 @@ +tdef_vty_test> list +... !timer + show timer [TNNNN] +... !timer + +tdef_vty_test> show timer ? + [TNNNN] T-number, optionally preceded by 't' or 'T'. + +tdef_vty_test> show timer +T1 = 100 s Testing a hundred seconds (default: 100 s) +T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +T3 = 100 m Testing a hundred minutes (default: 100 m) +T4 = 100 Testing a hundred potatoes (default: 100) +T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test> enable +tdef_vty_test# show timer +T1 = 100 s Testing a hundred seconds (default: 100 s) +T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +T3 = 100 m Testing a hundred minutes (default: 100 m) +T4 = 100 Testing a hundred potatoes (default: 100) +T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test# configure terminal +tdef_vty_test(config)# show running-config +... !timer + +tdef_vty_test(config)# network + +tdef_vty_test(config-net)# do show timer +T1 = 100 s Testing a hundred seconds (default: 100 s) +T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +T3 = 100 m Testing a hundred minutes (default: 100 m) +T4 = 100 Testing a hundred potatoes (default: 100) +T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config-net)# do show timer T3 +T3 = 100 m Testing a hundred minutes (default: 100 m) +tdef_vty_test(config-net)# do show timer 3 +T3 = 100 m Testing a hundred minutes (default: 100 m) +tdef_vty_test(config-net)# do show timer t3 +T3 = 100 m Testing a hundred minutes (default: 100 m) + +tdef_vty_test(config-net)# timer T1 5 +tdef_vty_test(config-net)# timer T1 +T1 = 5 s Testing a hundred seconds (default: 100 s) + +tdef_vty_test(config-net)# timer t1 678 +tdef_vty_test(config-net)# timer T1 +T1 = 678 s Testing a hundred seconds (default: 100 s) + +tdef_vty_test(config-net)# timer 1 9012345 +tdef_vty_test(config-net)# timer T1 +T1 = 9012345 s Testing a hundred seconds (default: 100 s) + +tdef_vty_test(config-net)# do show timer T666 +% No such timer: T666 +tdef_vty_test(config-net)# do show timer t666 +% No such timer: T666 +tdef_vty_test(config-net)# do show timer 666 +% No such timer: T666 + +tdef_vty_test(config-net)# timer T666 +% No such timer: T666 +tdef_vty_test(config-net)# timer t666 +% No such timer: T666 +tdef_vty_test(config-net)# timer 666 +% No such timer: T666 + +tdef_vty_test(config-net)# timer T666 5 +% No such timer: T666 + +tdef_vty_test(config-net)# timer T-23 42 +tdef_vty_test(config-net)# timer T-23 +T-23 = 42 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config-net)# timer t-23 43 +tdef_vty_test(config-net)# timer T-23 +T-23 = 43 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config-net)# timer -23 44 +tdef_vty_test(config-net)# timer T-23 +T-23 = 44 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config-net)# do show timer +T1 = 9012345 s Testing a hundred seconds (default: 100 s) +T2 = 100 ms Testing a hundred milliseconds (default: 100 ms) +T3 = 100 m Testing a hundred minutes (default: 100 m) +T4 = 100 Testing a hundred potatoes (default: 100) +T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m) +T-23 = 44 s Negative T number (default: 18446744073709551601 s) + +tdef_vty_test(config-net)# show running-config +... !timer +net + timer T1 9012345 + timer T-23 44 +... !timer + +tdef_vty_test(config-net)# timer T1 default +tdef_vty_test(config-net)# timer T-23 default + +tdef_vty_test(config-net)# show running-config +... !timer diff --git a/tests/tdef/tdef_vty_test_dynamic.c b/tests/tdef/tdef_vty_test_dynamic.c new file mode 100644 index 000000000..20dae535c --- /dev/null +++ b/tests/tdef/tdef_vty_test_dynamic.c @@ -0,0 +1,362 @@ +/* Test implementation for osmo_tdef VTY configuration API. */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include "config.h" + +void *root_ctx = NULL; + +/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */ + +/* This example keeps a separate list of timers for each instance of a dynamically allocated instance of a VTY node, + * for example of keeping separate timers for each BTS in a BSC. + */ + +static const struct osmo_tdef bts_default_tdefs[] = { + { .T=1111, .default_val=2, .desc="Dynamic Duo" }, + { .T=2222, .default_val=1, .desc="BATMAN" }, + { .T=3333, .default_val=12, .desc="Dadadadadadadadadadadada" }, + { .T=4444, .default_val=500, .unit=OSMO_TDEF_MS, .desc="POW!" }, + {} +}; + + +/* Boilerplate dynamic VTY node ... */ + +enum tdef_vty_test_nodes { + MEMBER_NODE = _LAST_OSMOVTY_NODE + 1, +}; + +static struct cmd_node member_node = { + MEMBER_NODE, + "%s(config-member)# ", + 1, +}; + +struct member { + struct llist_head entry; + char name[23]; + struct osmo_tdef *tdefs; +}; + +LLIST_HEAD(all_members); + +struct member *member_alloc(const char *name) +{ + struct member *m = talloc_zero(root_ctx, struct member); + osmo_strlcpy(m->name, name, sizeof(m->name)); + + /* DYNAMIC TDEF COPIES */ + m->tdefs = (struct osmo_tdef*)talloc_size(m, sizeof(bts_default_tdefs)); + memcpy((char*)m->tdefs, (char*)&bts_default_tdefs, sizeof(bts_default_tdefs)); + osmo_tdefs_reset(m->tdefs); + + llist_add_tail(&m->entry, &all_members); + return m; +} + +struct member *member_find(const char *name) +{ + struct member *m; + llist_for_each_entry(m, &all_members, entry) { + if (!strcmp(m->name, name)) + return m; + } + return NULL; +} + +DEFUN(cfg_member, cfg_member_cmd, + "member NAME", + "Enter member node\n" "Existing or new member node name\n") +{ + const char *name = argv[0]; + struct member *m = member_find(name); + if (!m) + m = member_alloc(name); + vty->index = m; + vty->node = MEMBER_NODE; + return CMD_SUCCESS; +} + + +/* TDEF SPECIFIC VTY */ + +static bool startswith(const char *str, const char *startswith_str) +{ + if (!startswith_str) + return true; + if (!str) + return false; + return strncmp(str, startswith_str, strlen(startswith_str)) == 0; +} + +DEFUN(show_timer, show_member_timer_cmd, + "show member-timer [NAME] " OSMO_TDEF_VTY_ARG_T_OPTIONAL, + SHOW_STR "Show timers for a specific member" "member name\n" + OSMO_TDEF_VTY_DOC_T) +{ + const char *name = argc > 0 ? argv[0] : NULL; + struct member *m; + const char *T_arg = argc > 1 ? argv[1] : NULL; + int shown = 0; + + llist_for_each_entry(m, &all_members, entry) { + if (!name || startswith(m->name, name)) { + osmo_tdef_vty_show_cmd(vty, m->tdefs, T_arg, "%11s: ", m->name); + shown ++; + } + } + if (!shown) { + vty_out(vty, "%% No such member: %s%s", name ? : "(none)", VTY_NEWLINE); + return CMD_WARNING; + } + return CMD_SUCCESS; +} + +DEFUN(cfg_member_timer, cfg_member_timer_cmd, + "timer " OSMO_TDEF_VTY_ARG_SET_OPTIONAL, + "Configure or show timers for this member\n" + OSMO_TDEF_VTY_DOC_SET) +{ + struct member *m = vty->index; + + if (!m || !m->tdefs) { + vty_out(vty, "%% No timers here%s", VTY_NEWLINE); + return CMD_WARNING; + } + + /* If any arguments are missing, redirect to 'show' */ + if (argc < 2) { + const char *T_arg = argc > 0 ? argv[0] : NULL; + return osmo_tdef_vty_show_cmd(vty, m->tdefs, T_arg, "%11s: ", m->name); + } + + return osmo_tdef_vty_set_cmd(vty, m->tdefs, argv); +} + +static int config_write_member(struct vty *vty) +{ + struct member *m; + llist_for_each_entry(m, &all_members, entry) { + vty_out(vty, "member %s%s", m->name, VTY_NEWLINE); + osmo_tdef_vty_write(vty, m->tdefs, " timer "); + } + + return CMD_SUCCESS; +} + +static void member_init_vty() +{ + install_node(&member_node, config_write_member); + install_element(CONFIG_NODE, &cfg_member_cmd); + + install_element_ve(&show_member_timer_cmd); + install_element(MEMBER_NODE, &cfg_member_timer_cmd); +} + +/* ------------------- THE REST is just boilerplate osmo main() ------------------- */ + +static void print_help() +{ + printf( "options:\n" + " -h --help this text\n" + " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n" + " -D --daemonize For the process into a background daemon\n" + " -c --config-file Specify the filename of the config file\n" + " -s --disable-color Don't use colors in stderr log output\n" + " -T --timestamp Prefix every log line with a timestamp\n" + " -V --version Print version information and exit\n" + " -e --log-level Set a global log-level\n" + ); +} + +static struct { + const char *config_file; + int daemonize; +} cmdline_config = {}; + +static void handle_options(int argc, char **argv) +{ + while (1) { + int option_idx = 0, c; + static const struct option long_options[] = { + { "help", 0, 0, 'h' }, + { "debug", 1, 0, 'd' }, + { "daemonize", 0, 0, 'D' }, + { "config-file", 1, 0, 'c' }, + { "disable-color", 0, 0, 's' }, + { "timestamp", 0, 0, 'T' }, + { "version", 0, 0, 'V' }, + { "log-level", 1, 0, 'e' }, + {} + }; + + c = getopt_long(argc, argv, "hc:d:Dc:sTVe:", + long_options, &option_idx); + if (c == -1) + break; + + switch (c) { + case 'h': + print_help(); + exit(0); + case 's': + log_set_use_color(osmo_stderr_target, 0); + break; + case 'd': + log_parse_category_mask(osmo_stderr_target, optarg); + break; + case 'D': + cmdline_config.daemonize = 1; + break; + case 'c': + cmdline_config.config_file = optarg; + break; + case 'T': + log_set_print_timestamp(osmo_stderr_target, 1); + break; + case 'e': + log_set_log_level(osmo_stderr_target, atoi(optarg)); + break; + case 'V': + print_version(1); + exit(0); + break; + default: + /* catch unknown options *as well as* missing arguments. */ + fprintf(stderr, "Error in command line options. Exiting.\n"); + exit(-1); + } + } +} + +static int quit = 0; + +static void signal_handler(int signal) +{ + fprintf(stdout, "signal %u received\n", signal); + + switch (signal) { + case SIGINT: + case SIGTERM: + quit++; + break; + case SIGABRT: + osmo_generate_backtrace(); + /* in case of abort, we want to obtain a talloc report + * and then return to the caller, who will abort the process */ + case SIGUSR1: + talloc_report(tall_vty_ctx, stderr); + talloc_report_full(root_ctx, stderr); + break; + case SIGUSR2: + talloc_report_full(tall_vty_ctx, stderr); + break; + default: + break; + } +} + +static struct vty_app_info vty_info = { + .name = "tdef_vty_test", + .version = PACKAGE_VERSION, +}; + +static const struct log_info_cat default_categories[] = {}; + +const struct log_info log_info = { + .cat = default_categories, + .num_cat = ARRAY_SIZE(default_categories), +}; + +int main(int argc, char **argv) +{ + int rc; + + root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test"); + + osmo_init_logging2(root_ctx, &log_info); + + vty_info.tall_ctx = root_ctx; + vty_init(&vty_info); + osmo_talloc_vty_add_cmds(); + + member_init_vty(); /* <--- relevant init for this example */ + + handle_options(argc, argv); + + if (cmdline_config.config_file) { + rc = vty_read_config_file(cmdline_config.config_file, NULL); + if (rc < 0) { + fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file); + return 1; + } + } + + rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042); + if (rc < 0) + return 2; + + signal(SIGINT, &signal_handler); + signal(SIGTERM, &signal_handler); + signal(SIGABRT, &signal_handler); + signal(SIGUSR1, &signal_handler); + signal(SIGUSR2, &signal_handler); + osmo_init_ignore_signals(); + + if (cmdline_config.daemonize) { + rc = osmo_daemonize(); + if (rc < 0) { + perror("Error during daemonize"); + return 6; + } + } + + while (!quit) { + log_reset_context(); + osmo_select_main(0); + } + + talloc_free(root_ctx); + talloc_free(tall_vty_ctx); + + return 0; +} diff --git a/tests/tdef/tdef_vty_test_dynamic.vty b/tests/tdef/tdef_vty_test_dynamic.vty new file mode 100644 index 000000000..6aae746bc --- /dev/null +++ b/tests/tdef/tdef_vty_test_dynamic.vty @@ -0,0 +1,83 @@ +tdef_vty_test> list +... + show member-timer [NAME] [TNNNN] +... + +tdef_vty_test> enable +tdef_vty_test# configure terminal + +tdef_vty_test(config)# member robin +tdef_vty_test(config-member)# timer + robin: T1111 = 2 s Dynamic Duo (default: 2 s) + robin: T2222 = 1 s BATMAN (default: 1 s) + robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + robin: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test(config-member)# timer T2222 423 +tdef_vty_test(config-member)# timer T2222 + robin: T2222 = 423 s BATMAN (default: 1 s) + +tdef_vty_test(config-member)# timer + robin: T1111 = 2 s Dynamic Duo (default: 2 s) + robin: T2222 = 423 s BATMAN (default: 1 s) + robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + robin: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test(config-member)# do show member-timer + robin: T1111 = 2 s Dynamic Duo (default: 2 s) + robin: T2222 = 423 s BATMAN (default: 1 s) + robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + robin: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test(config-member)# exit + +tdef_vty_test(config)# member batman +tdef_vty_test(config-member)# timer 3333 17 +tdef_vty_test(config-member)# timer 3333 + batman: T3333 = 17 s Dadadadadadadadadadadada (default: 12 s) + +tdef_vty_test(config-member)# show running-config + +Current configuration: +... +member robin + timer T2222 423 +member batman + timer T3333 17 +... + +tdef_vty_test(config-member)# timer 3333 default + +tdef_vty_test(config-member)# show running-config +... +member robin + timer T2222 423 +member batman +... !timer + +tdef_vty_test(config-member)# exit +tdef_vty_test(config)# exit +tdef_vty_test# show member-timer + robin: T1111 = 2 s Dynamic Duo (default: 2 s) + robin: T2222 = 423 s BATMAN (default: 1 s) + robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + robin: T4444 = 500 ms POW! (default: 500 ms) + batman: T1111 = 2 s Dynamic Duo (default: 2 s) + batman: T2222 = 1 s BATMAN (default: 1 s) + batman: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + batman: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test# show member-timer batman + batman: T1111 = 2 s Dynamic Duo (default: 2 s) + batman: T2222 = 1 s BATMAN (default: 1 s) + batman: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + batman: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test# show member-timer robin + robin: T1111 = 2 s Dynamic Duo (default: 2 s) + robin: T2222 = 423 s BATMAN (default: 1 s) + robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s) + robin: T4444 = 500 ms POW! (default: 500 ms) + +tdef_vty_test# show member-timer joker +% No such member: joker diff --git a/tests/testsuite.at b/tests/testsuite.at index 6aaaa787c..0093403a7 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -325,3 +325,9 @@ AT_KEYWORDS([gsm23003]) cat $abs_srcdir/gsm23003/gsm23003_test.ok > expout AT_CHECK([$abs_top_builddir/tests/gsm23003/gsm23003_test], [0], [expout], [ignore]) AT_CLEANUP + +AT_SETUP([tdef]) +AT_KEYWORDS([tdef]) +cat $abs_srcdir/tdef/tdef_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/tdef/tdef_test], [0], [expout], [ignore]) +AT_CLEANUP