diff --git a/doc/configuration.txt b/doc/configuration.txt index 2c00c8680a541..6ee547ca75677 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1817,6 +1817,7 @@ The following keywords are supported in the "global" section : - presetenv - prealloc-fd - resetenv + - server-rename - set-dumpable - set-var - setenv @@ -3209,6 +3210,14 @@ server-state-file configuration. See also "server-state-base" and "show servers state", "load-server-state-from-file" and "server-state-file-name" +server-rename + Enable runtime renaming of servers via the CLI for all backends globally. + When set, it allows the name of any server to be changed at runtime using + the CLI command "set server / name " without + requiring "option server-rename" in each backend individually. This option + supports the "no" prefix to disable it. See also: 'option server-rename' + for per-backend control. + set-dumpable [ on | off | libs ] This option helps choose the core dump behavior in case of process crash. Available options are: @@ -6187,6 +6196,7 @@ option pgsql-check X - X X option prefer-last-server (*) X - X X option redispatch (*) X - X X option redis-check X - X X +option server-rename - - X X option smtpchk X - X X option socket-stats (*) X X X - option splice-auto (*) X X X X @@ -6274,6 +6284,36 @@ use-server - - X X 4.2. Alphabetically sorted keywords reference + +option server-rename + Enable runtime renaming of servers via the CLI. + + May be used in the following contexts: tcp, http + + May be used in sections : defaults | frontend | listen | backend + no | no | no | yes + + When this option is enabled in a backend, it allows the name of any server in that backend to be changed at runtime using the CLI command: + set server / name + + Restrictions: + - The new name must be unique among all servers in the backend. + - The change is effective immediately for all runtime operations, stats, and logs. + - The new name is not persisted to the configuration file and will be lost on reload. + - Renaming is only available if 'option server-rename' is set in the backend + or 'server-rename' is set in the global section. + - Some features that rely on static server names (such as certain stick-tables or external monitoring) may not recognize the new name until restart. + + Example: + backend myapp + option server-rename + server s1 10.0.0.1:80 + server s2 10.0.0.2:80 + + To rename server 's1' to 'blue': + echo "set server myapp/s1 name blue" | socat /var/run/haproxy.sock - + + See also: 'server-rename' in the global section, 'set server / name' in the management guide (doc/management.txt). --------------------------------------------- This section provides a description of each keyword and its usage. diff --git a/doc/management.txt b/doc/management.txt index ee36f703722cd..5afc85bbe6999 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -2688,6 +2688,36 @@ set server / ssl [ on | off ] (deprecated) This command is deprecated, create a new server dynamically with or without SSL instead, using the "add server" command. +set server / name + Change the name of a server at runtime (if 'option server-rename' + is enabled in the backend or 'server-rename' is set in the global section). + + The new name must be unique among all servers in the backend. The + change is effective immediately for all runtime operations, stats, + and logs, but is not persisted to the configuration file and will + be lost on reload. Some features that rely on static server names + (such as stick-tables or external monitoring) may not recognize + the new name until restart. + + There is no strict requirement that no sessions are active + before renaming. However, if there are active sessions, log entries + for those sessions may use either the old or new name, depending on + when the rename occurs relative to logging. This may result in split + or inconsistent logs for in-flight requests. If strict log consistency + is required, it is the user's responsibility to ensure all sessions + have finished before renaming (e.g., by draining or maintenance mode + and waiting for all connections to close). + + For most use cases, a clear naming scheme (such as srv1 → srv1-old, + srv1-new, or srv1*) allows correlating log entries before and after + the rename, even if some overlap occurs. Users should choose a naming + convention that fits their operational and troubleshooting needs. + + Example: + set server myapp/s1 name blue + + See also: 'option server-rename' in doc/configuration.txt. + set severity-output [ none | number | string ] Change the severity output format of the stats socket connected to for the duration of the current session. diff --git a/include/haproxy/event_hdl-t.h b/include/haproxy/event_hdl-t.h index 85d13bea7205a..8b34401749713 100644 --- a/include/haproxy/event_hdl-t.h +++ b/include/haproxy/event_hdl-t.h @@ -279,6 +279,8 @@ struct event_hdl_sub { #define EVENT_HDL_SUB_SERVER_CHECK EVENT_HDL_SUB_TYPE(1,7) /* server inet addr (addr:svc_port tuple) change event */ #define EVENT_HDL_SUB_SERVER_INETADDR EVENT_HDL_SUB_TYPE(1,8) +/* server name change event */ +#define EVENT_HDL_SUB_SERVER_NAME EVENT_HDL_SUB_TYPE(1,9) /* PAT_REF family, only published in pat ref subscription list * (not published in global subscription list for performance reasons) diff --git a/include/haproxy/global-t.h b/include/haproxy/global-t.h index 2ffd8305f7038..c657f73315ed0 100644 --- a/include/haproxy/global-t.h +++ b/include/haproxy/global-t.h @@ -80,6 +80,7 @@ #define GTUNE_DISABLE_ACTIVE_CLOSE (1<<22) #define GTUNE_QUICK_EXIT (1<<23) #define GTUNE_COLLECT_LIBS (1<<24) +#define GTUNE_SRV_RENAME (1<<25) /* (1<<25) unused */ #define GTUNE_USE_FAST_FWD (1<<26) #define GTUNE_LISTENER_MQ_FAIR (1<<27) diff --git a/include/haproxy/proxy-t.h b/include/haproxy/proxy-t.h index acc84b647cf3e..9ab2996167124 100644 --- a/include/haproxy/proxy-t.h +++ b/include/haproxy/proxy-t.h @@ -188,7 +188,9 @@ enum PR_SRV_STATE_FILE { #define PR_O3_HASHAFNTY_MAXQUEUE 0x00000200 /* preserve hash affinity until maxqueue is reached */ #define PR_O3_HASHAFNTY_MASK 0x00000300 /* mask for hash-preserve-affinity */ -/* unused: 0x00000400 to 0x80000000 */ +#define PR_O3_SRV_RENAME 0x00000400 /* allow runtime server renaming */ + +/* unused: 0x00000800 to 0x80000000 */ /* end of proxy->options3 */ /* Cookie settings for pr->ck_opts */ diff --git a/include/haproxy/server-t.h b/include/haproxy/server-t.h index c8f318c5ce541..052f30d3e4d35 100644 --- a/include/haproxy/server-t.h +++ b/include/haproxy/server-t.h @@ -566,6 +566,7 @@ struct event_hdl_cb_data_server { * EVENT_HDL_SUB_SERVER_ADMIN * EVENT_HDL_SUB_SERVER_CHECK * EVENT_HDL_SUB_SERVER_INETADDR + * EVENT_HDL_SUB_SERVER_NAME */ struct { /* safe data can be safely used from both @@ -769,6 +770,24 @@ struct event_hdl_cb_data_server_inetaddr { /* no unsafe data */ }; +/* data provided to EVENT_HDL_SUB_SERVER_NAME handlers through + * event_hdl facility + * + * Note that this may be casted to regular event_hdl_cb_data_server if + * you don't care about name related optional info + */ +struct event_hdl_cb_data_server_name { + /* provided by: + * EVENT_HDL_SUB_SERVER_NAME + */ + struct event_hdl_cb_data_server server; /* must be at the beginning */ + struct { + char old_name[64]; + char new_name[64]; + } safe; + /* no unsafe data */ +}; + /* Storage structure to load server-state lines from a flat file into * an ebtree, for faster processing */ diff --git a/reg-tests/server/cli_set_server_name.vtc b/reg-tests/server/cli_set_server_name.vtc new file mode 100644 index 0000000000000..35b1c6c3904f0 --- /dev/null +++ b/reg-tests/server/cli_set_server_name.vtc @@ -0,0 +1,186 @@ +varnishtest "Set server name via CLI" + +feature ignore_unknown_macro + +#REGTEST_TYPE=devel + +# Do nothing. Is there only to create s1_* macros +server s1 { + rxreq + txresp +} -start + +haproxy h1 -conf { + global + .if feature(THREAD) + thread-groups 1 + .endif + + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${feS}" + default_backend test + + backend test + option server-rename + balance random + server s1 ${s1_addr}:${s1_port} + server s2 ${s1_addr}:${s1_port} + + backend norename + balance random + server s1 ${s1_addr}:${s1_port} + + backend useserver + option server-rename + balance random + server s1 ${s1_addr}:${s1_port} + server s2 ${s1_addr}:${s1_port} + use-server s1 if { always_true } + + backend tracked + option server-rename + balance random + server s1 ${s1_addr}:${s1_port} + server s2 ${s1_addr}:${s1_port} track tracked/s1 +} -start + +haproxy h1 -cli { + # ---- error cases ---- + + # missing new name argument + send "set server test/s1 name" + expect ~ "set server / name requires a new name." + + # backend without 'option server-rename' + send "set server norename/s1 name newname" + expect ~ "Backend does not allow server renaming" + + # server not in maintenance mode + send "set server test/s1 name newname" + expect ~ "Server must be in maintenance mode" + + # put s1 in maintenance for subsequent tests + send "disable server test/s1" + expect ~ ".*" + + # name containing '/' is rejected + send "set server test/s1 name bad/name" + expect ~ "Server name must not contain spaces, control characters, or '/'." + + # duplicate name — s2 already exists + send "set server test/s1 name s2" + expect ~ "A server with the same name already exists" + + # server targeted by a static use-server rule + send "disable server useserver/s1" + expect ~ ".*" + send "set server useserver/s1 name newname" + expect ~ "referenced by a 'use-server' rule" + + # server tracked by another server + send "disable server tracked/s1" + expect ~ ".*" + send "set server tracked/s1 name newname" + expect ~ "tracked by another server" + + # renaming to the same name is a no-op (success) + send "set server test/s1 name s1" + expect ~ "Server name updated." + + # ---- success case ---- + + # rename s1 -> blue + send "set server test/s1 name blue" + expect ~ "Server name updated." + + # verify the new name appears in show servers state + send "show servers state test" + expect ~ "test 1 blue" + + # the old name should no longer resolve + send "set server test/s1 state ready" + expect ~ "No such server." + + # new name is usable via enable + send "enable server test/blue" + expect ~ ".*" + + # ---- rename back ---- + + # must go through maintenance again + send "disable server test/blue" + expect ~ ".*" + + send "set server test/blue name s1" + expect ~ "Server name updated." + + send "show servers state test" + expect ~ "test 1 s1" + + # bring back up + send "enable server test/s1" + expect ~ ".*" +} + +# verify traffic still works after rename round-trip +client c1 -connect ${h1_feS_sock} { + txreq + rxresp + expect resp.status == 200 +} -run + +# ---- test global server-rename ---- + +server s2 { + rxreq + txresp +} -start + +haproxy h2 -conf { + global + server-rename + .if feature(THREAD) + thread-groups 1 + .endif + + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${feS}" + default_backend test + + backend test + balance random + server s1 ${s2_addr}:${s2_port} +} -start + +haproxy h2 -cli { + # global server-rename allows renaming without per-backend option + send "disable server test/s1" + expect ~ ".*" + + send "set server test/s1 name green" + expect ~ "Server name updated." + + send "show servers state test" + expect ~ "test 1 green" + + send "enable server test/green" + expect ~ ".*" +} + +client c2 -connect ${h2_feS_sock} { + txreq + rxresp + expect resp.status == 200 +} -run diff --git a/src/cfgparse-global.c b/src/cfgparse-global.c index d16f9747e242e..734fb25ebd95c 100644 --- a/src/cfgparse-global.c +++ b/src/cfgparse-global.c @@ -36,6 +36,7 @@ int cluster_secret_isset; static const char *common_kw_list[] = { "global", "busy-polling", "set-dumpable", "insecure-fork-wanted", "insecure-setuid-wanted", "nosplice", + "server-rename", "nogetaddrinfo", "noreuseport", "uid", "gid", "external-check", "user", "group", "maxconn", "ssl-server-verify", "maxconnrate", "maxsessrate", "maxsslrate", @@ -123,6 +124,14 @@ int cfg_parse_global(const char *file, int linenum, char **args, int kwm) else global.tune.options |= GTUNE_INSECURE_FORK; } + else if (strcmp(args[0], "server-rename") == 0) { /* "no server-rename" or "server-rename" */ + if (alertif_too_many_args(0, file, linenum, args, &err_code)) + goto out; + if (kwm == KWM_NO) + global.tune.options &= ~GTUNE_SRV_RENAME; + else + global.tune.options |= GTUNE_SRV_RENAME; + } else if (strcmp(args[0], "insecure-setuid-wanted") == 0) { /* "no insecure-setuid-wanted" or "insecure-setuid-wanted" */ if (alertif_too_many_args(0, file, linenum, args, &err_code)) goto out; diff --git a/src/proxy.c b/src/proxy.c index 5656a51d5fffa..e63419026bf3b 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -158,6 +158,7 @@ const struct cfg_opt cfg_opts3[] = { {"assume-rfc6587-ntf", PR_O3_ASSUME_RFC6587_NTF, PR_CAP_FE, 0, PR_MODE_SYSLOG }, {"dont-parse-log", PR_O3_DONTPARSELOG, PR_CAP_FE, 0, PR_MODE_SYSLOG }, + {"server-rename", PR_O3_SRV_RENAME, PR_CAP_BE, 0, 0 }, { NULL, 0, 0, 0 } }; diff --git a/src/server.c b/src/server.c index 8e49f79cf3ca2..ecb8167ef0539 100644 --- a/src/server.c +++ b/src/server.c @@ -30,11 +30,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -46,9 +48,10 @@ #include #include #include -#include #include +#include #include +#include #include #include #include @@ -56,7 +59,6 @@ #include #include #include -#include static void srv_update_status(struct server *s, int type, int cause); static int srv_apply_lastaddr(struct server *srv, int *err_code); @@ -5464,6 +5466,143 @@ const char *srv_update_fqdn(struct server *server, const char *fqdn, const char } +/* Rename a server at runtime. This function handles all precondition checks, + * tree re-indexing under thread_isolate(), and event publication. + * + * The caller must NOT hold any lock — this function uses thread_isolate() + * internally for tree manipulation. + * + * Preconditions enforced: + * - backend must have "option server-rename" set + * - server must be administratively in maintenance + * - new name must not conflict with an existing server in the backend + * - new name must be syntactically valid (no spaces, '/', '#' prefix) + * - server must not be targeted by a static 'use-server' rule + * - server must not be tracked by another server ('track' directive) + * - backend must not have sticking rules referencing a peer-synced table + * + * Note: sample fetches that take a server argument by name (srv_queue(), + * srv_conn(), srv_is_up(), etc.) are also resolved to pointers at config-check + * time and therefore continue to work after a rename, but will leave the config + * text inconsistent with the running state. Detecting all such references would + * require walking every ACL/rule expression tree in the proxy and is not + * currently implemented. Operators should audit their ACL expressions manually + * before renaming a server. + * + * Returns NULL on success, or a pointer to a static/trash error message + * string on failure. On success, a ha_notice() is emitted and the + * EVENT_HDL_SUB_SERVER_NAME event is published. + */ +static const char *srv_update_server_name(struct server *srv, const char *new_name) +{ + struct proxy *be = srv->proxy; + struct sticking_rule *rule; + struct server_rule *srule; + char *old_name; + char *dup; + const char *p; + + /* validate the new name is not empty */ + if (!*new_name) + return "Require a new server name.\n"; + + /* reject names that are too long for the event data structure */ + if (strlen(new_name) >= sizeof(((struct event_hdl_cb_data_server_name *)0)->safe.new_name)) + return "Server name too long.\n"; + + /* reject names starting with '#' (numeric ID syntax in server_find) */ + if (*new_name == '#') + return "Server name must not start with '#'.\n"; + + /* reject control characters, spaces, and '/' which break CLI parsing */ + for (p = new_name; *p; p++) { + if ((unsigned char)*p <= ' ' || *p == '/') + return "Server name must not contain spaces, control characters, or '/'.\n"; + } + + /* per-backend or global opt-in required */ + if (!(be->options3 & PR_O3_SRV_RENAME) && !(global.tune.options & GTUNE_SRV_RENAME)) + return "Backend does not allow server renaming (add 'option server-rename' or global 'server-rename').\n"; + + /* server must be administratively down (in maintenance) */ + if (!(srv->cur_admin & SRV_ADMF_MAINT)) + return "Server must be in maintenance mode to be renamed (set server / state maint).\n"; + + /* reject if a static 'use-server' rule hard-codes this server's name: + * dynamic rules use a logformat expression so srv.ptr is NULL there. + */ + list_for_each_entry(srule, &be->server_rules, list) { + if (!srule->dynamic && srule->srv.ptr == srv) + return "Cannot rename: server is referenced by a 'use-server' rule.\n"; + } + + /* reject if another server tracks this one via 'track': renaming would + * make the running state inconsistent with the config text. + */ + if (srv->trackers) + return "Cannot rename: server is tracked by another server ('track' directive).\n"; + + /* reject if any sticking rule references a peer-synced table */ + list_for_each_entry(rule, &be->sticking_rules, list) { + if (rule->table.t && rule->table.t->peers.p) + return "Cannot rename: backend has sticking rules using a peer-synced table.\n"; + } + list_for_each_entry(rule, &be->storersp_rules, list) { + if (rule->table.t && rule->table.t->peers.p) + return "Cannot rename: backend has store-response rules using a peer-synced table.\n"; + } + + /* same name is a no-op success */ + if (strcmp(srv->id, new_name) == 0) + return NULL; + + /* allocate new name before taking isolation */ + dup = strdup(new_name); + if (!dup) + return "Out of memory allocating new server name.\n"; + + /* tree manipulation requires thread isolation (same pattern as + * add/del server). This is rare enough that the cost is acceptable. + */ + thread_isolate(); + + /* re-check for name conflict under isolation — another rename or + * add server could have raced before we isolated. + */ + if (server_find_by_name(be, new_name)) { + thread_release(); + free(dup); + return "A server with the same name already exists in this backend.\n"; + } + + /* --- point of no return --- */ + + old_name = srv->id; + + /* re-index in the name tree */ + cebis_item_delete(&be->conf.used_server_name, conf.name_node, id, srv); + srv->id = dup; + cebis_item_insert(&be->conf.used_server_name, conf.name_node, id, srv); + + /* publish rename event with both old and new names */ + { + struct event_hdl_cb_data_server_name cb_data; + + _srv_event_hdl_prepare(&cb_data.server, srv, 1); + snprintf(cb_data.safe.old_name, sizeof(cb_data.safe.old_name), "%s", old_name); + snprintf(cb_data.safe.new_name, sizeof(cb_data.safe.new_name), "%s", new_name); + _srv_event_hdl_publish(EVENT_HDL_SUB_SERVER_NAME, cb_data, srv); + } + + thread_release(); + + ha_notice("Server %s/%s renamed from '%s'.\n", be->id, srv->id, old_name); + free(old_name); + + return NULL; +} + + /* Expects to find a backend and a server in under the form /, * and returns the pointer to the server. Otherwise, display adequate error messages * on the CLI, sets the CLI's state to CLI_ST_PRINT and returns NULL. This is only @@ -5721,11 +5860,26 @@ static int cli_parse_set_server(char **args, char *payload, struct appctx *appct #else cli_msg(appctx, LOG_NOTICE, "server ssl setting not supported.\n"); #endif - } else { + } + else if (strcmp(args[3], "name") == 0) { + if (!*args[4]) { + cli_err(appctx, "set server / name requires a new name.\n"); + goto out; + } + /* srv_update_server_name() handles its own locking via + * thread_isolate(), so do NOT hold the server lock here. + */ + warning = srv_update_server_name(sv, args[4]); + if (warning) + cli_err(appctx, warning); + else + cli_msg(appctx, LOG_NOTICE, "Server name updated.\n"); + } + else { cli_err(appctx, "usage: set server / " "addr | agent | agent-addr | agent-port | agent-send | " - "check-addr | check-port | fqdn | health | ssl | " + "check-addr | check-port | fqdn | health | name | ssl | " "state | weight\n"); } out: @@ -6620,7 +6774,7 @@ static struct cli_kw_list cli_kws = {{ },{ { { "enable", "health", NULL }, "enable health : enable health checks", cli_parse_enable_health, NULL }, { { "enable", "server", NULL }, "enable server (DEPRECATED) : enable a disabled server (use 'set server' instead)", cli_parse_enable_server, NULL }, { { "set", "maxconn", "server", NULL }, "set maxconn server / : change a server's maxconn setting", cli_parse_set_maxconn_server, NULL }, - { { "set", "server", NULL }, "set server / [opts] : change a server's state, weight, address or ssl", cli_parse_set_server }, + { { "set", "server", NULL }, "set server / [opts] : change a server's state, weight, address, name or ssl", cli_parse_set_server }, { { "get", "weight", NULL }, "get weight / : report a server's current weight", cli_parse_get_weight }, { { "set", "weight", NULL }, "set weight / (DEPRECATED) : change a server's weight (use 'set server' instead)", cli_parse_set_weight }, { { "add", "server", NULL }, "add server / : create a new server", cli_parse_add_server, cli_io_handler_add_server },