Thierry, Something for Monday :-)
Latest version of the patch in attachment: - Filter table format is flattened/simplified - I've tried to address filter table format error messages (what is the error, and which filter entry is wrong) - Fixed one bug: stktable_get_data_type() return value can be < 0 (i.e. filter table contains unknown data column) I've run a few unit tests for my Lua code using stick tables, hitting all methods and handling regular return values and filter table errors. So far so good, it looks good on my side. Best regards, Adis
>From 6b702ff6f12f919ba4d2f42a7962fa2345272382 Mon Sep 17 00:00:00 2001 From: Adis Nezirovic <[email protected]> Date: Fri, 13 Jul 2018 12:18:33 +0200 Subject: [PATCH] MEDIUM: lua: Add stick table support for Lua. This ads support for accessing stick tables from Lua. The supported operations are reading general table info, lookup by string/IP key, and dumping the table. Similar to "show table", a data filter is available during dump, and as an improvement over "show table" it's possible to use up to 4 filter expressions instead of just one (with implicit AND clause binding the expressions). Dumping with/without filters can take a long time for large tables, and should be used sparingly. --- doc/lua-api/index.rst | 72 ++++++++ include/types/hlua.h | 1 + src/hlua_fcn.c | 399 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+) diff --git a/doc/lua-api/index.rst b/doc/lua-api/index.rst index 0c79766e..9a2e039a 100644 --- a/doc/lua-api/index.rst +++ b/doc/lua-api/index.rst @@ -852,6 +852,10 @@ Proxy class Contain a table with the attached servers. The table is indexed by server name, and each server entry is an object of type :ref:`server_class`. +.. js:attribute:: Proxy.stktable + + Contains a stick table object attached to the proxy. + .. js:attribute:: Proxy.listeners Contain a table with the attached listeners. The table is indexed by listener @@ -2489,6 +2493,74 @@ AppletTCP class :see: :js:func:`AppletTCP.unset_var` :see: :js:func:`AppletTCP.set_var` +StickTable class +================ + +.. js:class:: StickTable + + **context**: task, action, sample-fetch + + This class can be used to access the HAProxy stick tables from Lua. + +.. js:function:: StickTable.info() + + Returns stick table attributes as a Lua table. See HAProxy documentation for + "stick-table" for canonical info, or check out example bellow. + + :returns: Lua table + + Assume our table has IPv4 key and gpc0 and conn_rate "columns": + +.. code-block:: lua + + { + expire=<int>, # Value in ms + size=<int>, # Maximum table size + used=<int>, # Actual number of entries in table + data={ # Data columns, with types as key, and periods as values + (-1 if type is not rate counter) + conn_rate=<int>, + gpc0=-1 + }, + length=<int>, # max string length for string table keys, key length + # otherwise + nopurge=<boolean>, # purge oldest entries when table is full + type="ip" # can be "ip", "ipv6", "integer", "string", "binary" + } + +.. js:function:: StickTable.lookup(key) + + Returns stick table entry for given <key> + + :param string key: Stick table key (IP addresses and strings are supported) + :returns: Lua table + +.. js:function:: StickTable.dump([filter]) + + Returns all entries in stick table. An optional filter can be used + to extract entries with specific data values. Filter is a table with valid + comparison operators as keys followed by data type name and value pairs. + Check out the HAProxy docs for "show table" for more details. For the + reference, the supported operators are: + "eq", "ne", "le", "lt", "ge", "gt" + + For large tables, execution of this function can take a long time (for + HAProxy standards). That's also true when filter is used, so take care and + measure the impact. + + :param table filter: Stick table filter + :returns: Stick table entries (table) + + See below for example filter, which contains 4 entries (or comparisons). + (Maximum number of filter entries is 4, defined in the source code) + +.. code-block:: lua + + local filter = { + {"gpc0", "gt", 30}, {"gpc1", "gt", 20}}, {"conn_rate", "le", 10} + } + + External Lua libraries ====================== diff --git a/include/types/hlua.h b/include/types/hlua.h index 5a8173f3..2e453351 100644 --- a/include/types/hlua.h +++ b/include/types/hlua.h @@ -25,6 +25,7 @@ #define CLASS_SERVER "Server" #define CLASS_LISTENER "Listener" #define CLASS_REGEX "Regex" +#define CLASS_STKTABLE "StickTable" struct stream; diff --git a/src/hlua_fcn.c b/src/hlua_fcn.c index cebce224..6f6d8380 100644 --- a/src/hlua_fcn.c +++ b/src/hlua_fcn.c @@ -30,6 +30,7 @@ #include <proto/proxy.h> #include <proto/server.h> #include <proto/stats.h> +#include <proto/stick_table.h> /* Contains the class reference of the concat object. */ static int class_concat_ref; @@ -37,7 +38,9 @@ static int class_proxy_ref; static int class_server_ref; static int class_listener_ref; static int class_regex_ref; +static int class_stktable_ref; +#define MAX_STK_FILTER_LEN 4 #define STATS_LEN (MAX((int)ST_F_TOTAL_FIELDS, (int)INF_TOTAL_FIELDS)) static THREAD_LOCAL struct field stats[STATS_LEN]; @@ -49,6 +52,38 @@ int hlua_checkboolean(lua_State *L, int index) return lua_toboolean(L, index); } +/* Helper to push unsigned integers to Lua stack, respecting Lua limitations */ +static int hlua_fcn_pushunsigned(lua_State *L, unsigned int val) +{ +#if (LUA_MAXINTEGER == LLONG_MAX || ((LUA_MAXINTEGER == LONG_MAX) && (__WORDSIZE == 64))) + lua_pushinteger(L, val); +#else + if (val > INT_MAX) + lua_pushnumber(L, (lua_Number)val); + else + lua_pushinteger(L, (int)val); +#endif + return 1; +} + +/* Helper to push unsigned long long to Lua stack, respecting Lua limitations */ +static int hlua_fcn_pushunsigned_ll(lua_State *L, unsigned long long val) { +#if (LUA_MAXINTEGER == LLONG_MAX || ((LUA_MAXINTEGER == LONG_MAX) && (__WORDSIZE == 64))) + /* 64 bits case, U64 is supported until LLONG_MAX */ + if (val > LLONG_MAX) + lua_pushnumber(L, (lua_Number)val); + else + lua_pushinteger(L, val); +#else + /* 32 bits case, U64 is supported until INT_MAX */ + if (val > INT_MAX) + lua_pushnumber(L, (lua_Number)val); + else + lua_pushinteger(L, (int)val); +#endif + return 1; +} + /* This function gets a struct field and convert it in Lua * variable. The variable is pushed at the top of the stak. */ @@ -446,6 +481,354 @@ static int hlua_concat_init(lua_State *L) return 1; } +int hlua_fcn_new_stktable(lua_State *L, struct stktable *tbl) +{ + lua_newtable(L); + + /* Pop a class stktbl metatable and affect it to the userdata. */ + lua_rawgeti(L, LUA_REGISTRYINDEX, class_stktable_ref); + lua_setmetatable(L, -2); + + lua_pushlightuserdata(L, tbl); + lua_rawseti(L, -2, 0); + return 1; +} + +static struct stktable *hlua_check_stktable(lua_State *L, int ud) +{ + return hlua_checkudata(L, ud, class_stktable_ref); +} + +/* Extract stick table attributes into Lua table */ +int hlua_stktable_info(lua_State *L) +{ + struct stktable *tbl; + int dt; + + tbl = hlua_check_stktable(L, 1); + + if (!tbl->id) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + + lua_pushstring(L, "type"); + lua_pushstring(L, stktable_types[tbl->type].kw); + lua_settable(L, -3); + + lua_pushstring(L, "length"); + lua_pushinteger(L, tbl->key_size); + lua_settable(L, -3); + + lua_pushstring(L, "size"); + hlua_fcn_pushunsigned(L, tbl->size); + lua_settable(L, -3); + + lua_pushstring(L, "used"); + hlua_fcn_pushunsigned(L, tbl->current); + lua_settable(L, -3); + + lua_pushstring(L, "nopurge"); + lua_pushboolean(L, tbl->nopurge > 0); + lua_settable(L, -3); + + lua_pushstring(L, "expire"); + lua_pushinteger(L, tbl->expire); + lua_settable(L, -3); + + /* Save data types periods (if applicable) in 'data' table */ + lua_pushstring(L, "data"); + lua_newtable(L); + + for (dt = 0; dt < STKTABLE_DATA_TYPES; dt++) { + if (tbl->data_ofs[dt] == 0) + continue; + + lua_pushstring(L, stktable_data_types[dt].name); + + if (stktable_data_types[dt].arg_type == ARG_T_DELAY) + lua_pushinteger(L, tbl->data_arg[dt].u); + else + lua_pushinteger(L, -1); + + lua_settable(L, -3); + } + + lua_settable(L, -3); + + return 1; +} + +/* Helper to get extract stick table entry into Lua table */ +static void hlua_stktable_entry(lua_State *L, struct stktable *t, struct stksess *ts) +{ + int dt; + void *ptr; + + for (dt = 0; dt < STKTABLE_DATA_TYPES; dt++) { + + if (t->data_ofs[dt] == 0) + continue; + + lua_pushstring(L, stktable_data_types[dt].name); + + ptr = stktable_data_ptr(t, ts, dt); + switch (stktable_data_types[dt].std_type) { + case STD_T_SINT: + lua_pushinteger(L, stktable_data_cast(ptr, std_t_sint)); + break; + case STD_T_UINT: + hlua_fcn_pushunsigned(L, stktable_data_cast(ptr, std_t_uint)); + break; + case STD_T_ULL: + hlua_fcn_pushunsigned_ll(L, stktable_data_cast(ptr, std_t_ull)); + break; + case STD_T_FRQP: + lua_pushinteger(L, read_freq_ctr_period(&stktable_data_cast(ptr, std_t_frqp), + t->data_arg[dt].u)); + break; + } + + lua_settable(L, -3); + } +} + +/* Looks in table <t> for a sticky session matching key <key> + * Returns table with session data or nil + * + * The returned table always contains 'use' and 'expire' (integer) fields. + * For frequency/rate counters, each data entry is returned as table with + * 'value' and 'period' fields. + */ +int hlua_stktable_lookup(lua_State *L) +{ + struct stktable *t; + struct sample smp; + struct stktable_key *skey; + struct stksess *ts; + + t = hlua_check_stktable(L, 1); + smp.data.type = SMP_T_STR; + smp.flags = SMP_F_CONST; + smp.data.u.str.area = (char *)luaL_checkstring(L, 2); + + skey = smp_to_stkey(&smp, t); + if (!skey) { + lua_pushnil(L); + return 1; + } + + ts = stktable_lookup_key(t, skey); + if (!ts) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + lua_pushstring(L, "use"); + lua_pushinteger(L, ts->ref_cnt - 1); + lua_settable(L, -3); + + lua_pushstring(L, "expire"); + lua_pushinteger(L, tick_remain(now_ms, ts->expire)); + lua_settable(L, -3); + + hlua_stktable_entry(L, t, ts); + HA_SPIN_LOCK(STK_TABLE_LOCK, &t->lock); + ts->ref_cnt--; + HA_SPIN_UNLOCK(STK_TABLE_LOCK, &t->lock); + + return 1; +} + +struct stk_filter { + long long val; + int type; + int op; +}; + + +/* Helper for returning errors to callers using Lua convention (nil, err) */ +static int hlua_error(lua_State *L, const char *fmt, ...) { + char buf[256]; + int len; + va_list args; + va_start(args, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + if (len < 0) { + ha_alert("hlua_error(): Could not write error message.\n"); + lua_pushnil(L); + return 1; + } else if (len >= sizeof(buf)) + ha_alert("hlua_error(): Error message was truncated.\n"); + + lua_pushnil(L); + lua_pushstring(L, buf); + + return 2; +} + +/* Dump the contents of stick table <t>*/ +int hlua_stktable_dump(lua_State *L) +{ + struct stktable *t; + struct ebmb_node *eb; + struct ebmb_node *n; + struct stksess *ts; + int type; + int op; + int dt; + long long val; + struct stk_filter filter[MAX_STK_FILTER_LEN]; + int filter_count = 0; + int i; + int skip_entry; + void *ptr; + + t = hlua_check_stktable(L, 1); + type = lua_type(L, 2); + + switch (type) { + case LUA_TNONE: + case LUA_TNIL: + break; + case LUA_TTABLE: + lua_pushnil(L); + while (lua_next(L, 2) != 0) { + int entry_idx = 0; + + if (filter_count >= MAX_STK_FILTER_LEN) + return hlua_error(L, "Filter table too large (len > %d)", MAX_STK_FILTER_LEN); + + if (lua_type(L, -1) != LUA_TTABLE || lua_rawlen(L, -1) != 3) + return hlua_error(L, "Filter table entry must be a triplet: {\"data_col\", \"op\", val} (entry #%d)", filter_count + 1); + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + switch (entry_idx) { + case 0: + if (lua_type(L, -1) != LUA_TSTRING) + return hlua_error(L, "Filter table data column must be string (entry #%d)", filter_count + 1); + + dt = stktable_get_data_type((char *)lua_tostring(L, -1)); + if (dt < 0 || t->data_ofs[dt] == 0) + return hlua_error(L, "Filter table data column not present in stick table (entry #%d)", filter_count + 1); + filter[filter_count].type = dt; + break; + case 1: + if (lua_type(L, -1) != LUA_TSTRING) + return hlua_error(L, "Filter table operator must be string (entry #%d)", filter_count + 1); + + op = get_std_op(lua_tostring(L, -1)); + if (op < 0) + return hlua_error(L, "Unknown operator in filter table (entry #%d)", filter_count + 1); + filter[filter_count].op = op; + break; + case 2: + val = lua_tointeger(L, -1); + filter[filter_count].val = val; + filter_count++; + break; + default: + break; + } + entry_idx++; + lua_pop(L, 1); + } + lua_pop(L, 1); + } + break; + default: + return hlua_error(L, "filter table expected"); + } + + lua_newtable(L); + + HA_SPIN_LOCK(STK_TABLE_LOCK, &t->lock); + eb = ebmb_first(&t->keys); + for (n = eb; n; n = ebmb_next(n)) { + ts = ebmb_entry(n, struct stksess, key); + if (!ts) { + HA_SPIN_UNLOCK(STK_TABLE_LOCK, &t->lock); + return 1; + } + ts->ref_cnt++; + HA_SPIN_UNLOCK(STK_TABLE_LOCK, &t->lock); + + /* multi condition/value filter */ + skip_entry = 0; + for (i = 0; i < filter_count; i++) { + if (t->data_ofs[filter[i].type] == 0) + continue; + + ptr = stktable_data_ptr(t, ts, filter[i].type); + + switch (stktable_data_types[filter[i].type].std_type) { + case STD_T_SINT: + val = stktable_data_cast(ptr, std_t_sint); + break; + case STD_T_UINT: + val = stktable_data_cast(ptr, std_t_uint); + break; + case STD_T_ULL: + val = stktable_data_cast(ptr, std_t_ull); + break; + case STD_T_FRQP: + val = read_freq_ctr_period(&stktable_data_cast(ptr, std_t_frqp), + t->data_arg[filter[i].type].u); + break; + default: + continue; + break; + } + + op = filter[i].op; + + if ((val < filter[i].val && (op == STD_OP_EQ || op == STD_OP_GT || op == STD_OP_GE)) || + (val == filter[i].val && (op == STD_OP_NE || op == STD_OP_GT || op == STD_OP_LT)) || + (val > filter[i].val && (op == STD_OP_EQ || op == STD_OP_LT || op == STD_OP_LE))) { + skip_entry = 1; + break; + } + } + + if (skip_entry) { + HA_SPIN_LOCK(STK_TABLE_LOCK, &t->lock); + ts->ref_cnt--; + continue; + } + + if (t->type == SMP_T_IPV4) { + char addr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, (const void *)&ts->key.key, addr, sizeof(addr)); + lua_pushstring(L, addr); + } else if (t->type == SMP_T_IPV6) { + char addr[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, (const void *)&ts->key.key, addr, sizeof(addr)); + lua_pushstring(L, addr); + } else if (t->type == SMP_T_SINT) { + lua_pushinteger(L, *ts->key.key); + } else if (t->type == SMP_T_STR) { + lua_pushstring(L, (const char *)ts->key.key); + } else { + return hlua_error(L, "Unsupported stick table key type"); + } + + lua_newtable(L); + hlua_stktable_entry(L, t, ts); + lua_settable(L, -3); + HA_SPIN_LOCK(STK_TABLE_LOCK, &t->lock); + ts->ref_cnt--; + } + HA_SPIN_UNLOCK(STK_TABLE_LOCK, &t->lock); + + return 1; +} + int hlua_fcn_new_listener(lua_State *L, struct listener *lst) { lua_newtable(L); @@ -887,6 +1270,12 @@ int hlua_fcn_new_proxy(lua_State *L, struct proxy *px) } lua_settable(L, -3); + if (px->table.id) { + lua_pushstring(L, "stktable"); + hlua_fcn_new_stktable(L, &px->table); + lua_settable(L, -3); + } + return 1; } @@ -1289,6 +1678,16 @@ int hlua_fcn_reg_core_fcn(lua_State *L) lua_setmetatable(L, -2); lua_setglobal(L, CLASS_REGEX); /* Create global object called Regex */ + /* Create stktable object. */ + lua_newtable(L); + lua_pushstring(L, "__index"); + lua_newtable(L); + hlua_class_function(L, "info", hlua_stktable_info); + hlua_class_function(L, "lookup", hlua_stktable_lookup); + hlua_class_function(L, "dump", hlua_stktable_dump); + lua_settable(L, -3); /* -> META["__index"] = TABLE */ + class_stktable_ref = hlua_register_metatable(L, CLASS_STKTABLE); + /* Create listener object. */ lua_newtable(L); lua_pushstring(L, "__index"); -- 2.18.0

