[MEDIUM] enable/disable servers from the stats web interface
Based on a patch provided by Judd Montgomery, it is now possible to
enable/disable servers from the stats web interface. This allows to select
several servers in a backend and apply the action to them at the same time.
Currently, there are 2 known limitations :
- The POST data are limited to one packet
(don't alter too many servers at a time).
- Expect: 100-continue is not supported.
(cherry picked from commit 7693948766cb5647ac03b48e782cfee2b1f14491)
diff --git a/include/proto/dumpstats.h b/include/proto/dumpstats.h
index 56d8abb..7038f46 100644
--- a/include/proto/dumpstats.h
+++ b/include/proto/dumpstats.h
@@ -53,6 +53,12 @@
#define STAT_CLI_O_ERR 7 /* dump errors */
#define STAT_CLI_O_TAB 8 /* dump tables */
+/* status codes (strictly 4 chars) used in the URL to display a message */
+#define STAT_STATUS_UNKN "UNKN" /* an unknown error occured, shouldn't happen */
+#define STAT_STATUS_DONE "DONE" /* the action is successful */
+#define STAT_STATUS_NONE "NONE" /* nothing happened (no action chosen or servers state didn't change) */
+#define STAT_STATUS_EXCD "EXCD" /* an error occured becayse the buffer couldn't store all data */
+
int stats_accept(struct session *s);
int stats_sock_parse_request(struct stream_interface *si, char *line);
diff --git a/include/types/session.h b/include/types/session.h
index d1e54ac..0bbb9bf 100644
--- a/include/types/session.h
+++ b/include/types/session.h
@@ -217,6 +217,7 @@
short px_st, sv_st; /* DATA_ST_INIT or DATA_ST_DATA */
unsigned int flags; /* STAT_* */
int iid, type, sid; /* proxy id, type and service id if bounding of stats is enabled */
+ const char *st_code; /* pointer to the status code returned by an action */
} stats;
struct {
struct bref bref; /* back-reference from the session being dumped */
diff --git a/src/dumpstats.c b/src/dumpstats.c
index 59607f9..5195d8c 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -1135,6 +1135,44 @@
}
+/* We don't want to land on the posted stats page because a refresh will
+ * repost the data. We don't want this to happen on accident so we redirect
+ * the browse to the stats page with a GET.
+ */
+int stats_http_redir(struct session *s, struct buffer *rep, struct uri_auth *uri)
+{
+ struct chunk msg;
+
+ chunk_init(&msg, trash, sizeof(trash));
+
+ switch (s->data_state) {
+ case DATA_ST_INIT:
+ chunk_printf(&msg,
+ "HTTP/1.0 303 See Other\r\n"
+ "Cache-Control: no-cache\r\n"
+ "Content-Type: text/plain\r\n"
+ "Connection: close\r\n"
+ "Location: %s;st=%s",
+ uri->uri_prefix, s->data_ctx.stats.st_code);
+ chunk_printf(&msg, "\r\n\r\n");
+
+ if (buffer_feed_chunk(rep, &msg) >= 0)
+ return 0;
+
+ s->txn.status = 303;
+
+ if (!(s->flags & SN_ERR_MASK)) // this is not really an error but it is
+ s->flags |= SN_ERR_PRXCOND; // to mark that it comes from the proxy
+ if (!(s->flags & SN_FINST_MASK))
+ s->flags |= SN_FINST_R;
+
+ s->data_state = DATA_ST_FIN;
+ return 1;
+ }
+ return 1;
+}
+
+
/* This I/O handler runs as an applet embedded in a stream interface. It is
* used to send HTTP stats over a TCP socket. The mechanism is very simple.
* si->st0 becomes non-zero once the transfer is finished. The handler
@@ -1154,9 +1192,16 @@
si->st0 = 1;
if (!si->st0) {
- if (stats_dump_http(s, res, s->be->uri_auth)) {
- si->st0 = 1;
- si->shutw(si);
+ if (s->txn.meth == HTTP_METH_POST) {
+ if (stats_http_redir(s, res, s->be->uri_auth)) {
+ si->st0 = 1;
+ si->shutw(si);
+ }
+ } else {
+ if (stats_dump_http(s, res, s->be->uri_auth)) {
+ si->st0 = 1;
+ si->shutw(si);
+ }
}
}
@@ -1446,6 +1491,39 @@
""
);
+ if (s->data_ctx.stats.st_code) {
+ if (strcmp(s->data_ctx.stats.st_code, STAT_STATUS_DONE) == 0) {
+ chunk_printf(&msg,
+ "<p><div class=active3>"
+ "<a class=lfsb href=\"%s\" title=\"Remove this message\">[X]</a> "
+ "Action processed successfully."
+ "</div>\n", uri->uri_prefix);
+ }
+ else if (strcmp(s->data_ctx.stats.st_code, STAT_STATUS_NONE) == 0) {
+ chunk_printf(&msg,
+ "<p><div class=active2>"
+ "<a class=lfsb href=\"%s\" title=\"Remove this message\">[X]</a> "
+ "Nothing has changed."
+ "</div>\n", uri->uri_prefix);
+ }
+ else if (strcmp(s->data_ctx.stats.st_code, STAT_STATUS_EXCD) == 0) {
+ chunk_printf(&msg,
+ "<p><div class=active0>"
+ "<a class=lfsb href=\"%s\" title=\"Remove this message\">[X]</a> "
+ "<b>Action not processed : the buffer couldn't store all the data.<br>"
+ "You should retry with less servers at a time.</b>"
+ "</div>\n", uri->uri_prefix);
+ }
+ else {
+ chunk_printf(&msg,
+ "<p><div class=active6>"
+ "<a class=lfsb href=\"%s\" title=\"Remove this message\">[X]</a> "
+ "Unexpected result."
+ "</div>\n", uri->uri_prefix);
+ }
+ chunk_printf(&msg,"<p>\n");
+ }
+
if (buffer_feed_chunk(rep, &msg) >= 0)
return 0;
}
@@ -1546,6 +1624,13 @@
case DATA_ST_PX_TH:
if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* A form to enable/disable this proxy servers */
+ chunk_printf(&msg,
+ "<form action=\"%s\" method=\"post\">",
+ uri->uri_prefix);
+ }
+
/* print a new table */
chunk_printf(&msg,
"<table class=\"tbl\" width=\"100%%\">\n"
@@ -1568,7 +1653,18 @@
"</tr>\n"
"</table>\n"
"<table class=\"tbl\" width=\"100%%\">\n"
- "<tr class=\"titre\">"
+ "<tr class=\"titre\">",
+ (uri->flags & ST_SHLGNDS)?"<u>":"",
+ px->id, px->id, px->id,
+ (uri->flags & ST_SHLGNDS)?"</u>":"",
+ px->desc ? "desc" : "empty", px->desc ? px->desc : "");
+
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* Column heading for Enable or Disable server */
+ chunk_printf(&msg, "<th rowspan=2 width=1></th>");
+ }
+
+ chunk_printf(&msg,
"<th rowspan=2></th>"
"<th colspan=3>Queue</th>"
"<th colspan=3>Session rate</th><th colspan=5>Sessions</th>"
@@ -1585,11 +1681,7 @@
"<th>Status</th><th>LastChk</th><th>Wght</th><th>Act</th>"
"<th>Bck</th><th>Chk</th><th>Dwn</th><th>Dwntme</th>"
"<th>Thrtle</th>\n"
- "</tr>",
- (uri->flags & ST_SHLGNDS)?"<u>":"",
- px->id, px->id, px->id,
- (uri->flags & ST_SHLGNDS)?"</u>":"",
- px->desc ? "desc" : "empty", px->desc ? px->desc : "");
+ "</tr>");
if (buffer_feed_chunk(rep, &msg) >= 0)
return 0;
@@ -1605,9 +1697,18 @@
if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
chunk_printf(&msg,
/* name, queue */
- "<tr class=\"frontend\"><td class=ac>"
+ "<tr class=\"frontend\">");
+
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* Column sub-heading for Enable or Disable server */
+ chunk_printf(&msg, "<td></td>");
+ }
+
+ chunk_printf(&msg,
+ "<td class=ac>"
"<a name=\"%s/Frontend\"></a>"
- "<a class=lfsb href=\"#%s/Frontend\">Frontend</a></td><td colspan=3></td>"
+ "<a class=lfsb href=\"#%s/Frontend\">Frontend</a></td>"
+ "<td colspan=3></td>"
"",
px->id, px->id);
@@ -1765,7 +1866,12 @@
}
if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
- chunk_printf(&msg, "<tr class=socket><td class=ac");
+ chunk_printf(&msg, "<tr class=socket>");
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* Column sub-heading for Enable or Disable server */
+ chunk_printf(&msg, "<td></td>");
+ }
+ chunk_printf(&msg, "<td class=ac");
if (uri->flags&ST_SHLGNDS) {
char str[INET6_ADDRSTRLEN], *fmt = NULL;
@@ -1939,16 +2045,21 @@
if ((sv->state & SRV_MAINTAIN) || (svs->state & SRV_MAINTAIN)) {
chunk_printf(&msg,
/* name */
- "<tr class=\"maintain\"><td class=ac"
+ "<tr class=\"maintain\">"
);
}
else {
chunk_printf(&msg,
/* name */
- "<tr class=\"%s%d\"><td class=ac",
+ "<tr class=\"%s%d\">",
(sv->state & SRV_BACKUP) ? "backup" : "active", sv_state);
}
+ chunk_printf(&msg,
+ "<td><input type=\"checkbox\" name=\"s\" value=\"%s\"></td>"
+ "<td class=ac",
+ sv->id);
+
if (uri->flags&ST_SHLGNDS) {
char str[INET6_ADDRSTRLEN];
@@ -2279,9 +2390,12 @@
if ((px->cap & PR_CAP_BE) &&
(!(s->data_ctx.stats.flags & STAT_BOUND) || (s->data_ctx.stats.type & (1 << STATS_TYPE_BE)))) {
if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
- chunk_printf(&msg,
- /* name */
- "<tr class=\"backend\"><td class=ac");
+ chunk_printf(&msg, "<tr class=\"backend\">");
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* Column sub-heading for Enable or Disable server */
+ chunk_printf(&msg, "<td></td>");
+ }
+ chunk_printf(&msg, "<td class=ac");
if (uri->flags&ST_SHLGNDS) {
/* balancing */
@@ -2305,6 +2419,7 @@
}
chunk_printf(&msg,
+ /* name */
">%s<a name=\"%s/Backend\"></a>"
"<a class=lfsb href=\"#%s/Backend\">Backend</a>%s</td>"
/* queue : current, max */
@@ -2467,7 +2582,24 @@
case DATA_ST_PX_END:
if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
- chunk_printf(&msg, "</table><p>\n");
+ chunk_printf(&msg, "</table>");
+
+ if (px->cap & PR_CAP_BE && px->srv) {
+ /* close the form used to enable/disable this proxy servers */
+ chunk_printf(&msg,
+ "Choose the action to perform on the checked servers : "
+ "<select name=action>"
+ "<option value=\"\"></option>"
+ "<option value=\"disable\">Disable</option>"
+ "<option value=\"enable\">Enable</option>"
+ "</select>"
+ "<input type=\"hidden\" name=\"b\" value=\"%s\">"
+ " <input type=\"submit\" value=\"Apply\">"
+ "</form>",
+ px->id);
+ }
+
+ chunk_printf(&msg, "<p>\n");
if (buffer_feed_chunk(rep, &msg) >= 0)
return 0;
diff --git a/src/proto_http.c b/src/proto_http.c
index 859b7fc..3d408e1 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -2839,6 +2839,110 @@
return 0;
}
+/* We reached the stats page through a POST request.
+ * Parse the posted data and enable/disable servers if necessary.
+ * Returns 0 if request was parsed.
+ * Returns 1 if there was a problem parsing the posted data.
+ */
+int http_process_req_stat_post(struct session *s, struct buffer *req)
+{
+ struct http_txn *txn = &s->txn;
+ struct proxy *px;
+ struct server *sv;
+
+ char *backend = NULL;
+ int action = 0;
+
+ char *first_param, *cur_param, *next_param, *end_params;
+
+ first_param = req->data + txn->req.eoh + 2;
+ end_params = first_param + txn->req.hdr_content_len;
+
+ cur_param = next_param = end_params;
+
+ if (end_params >= req->data + req->size) {
+ /* Prevent buffer overflow */
+ s->data_ctx.stats.st_code = STAT_STATUS_EXCD;
+ return 1;
+ }
+ else if (end_params > req->data + req->l) {
+ /* This condition also rejects a request with Expect: 100-continue */
+ s->data_ctx.stats.st_code = STAT_STATUS_EXCD;
+ return 1;
+ }
+
+ *end_params = '\0';
+
+ s->data_ctx.stats.st_code = STAT_STATUS_NONE;
+
+ /*
+ * Parse the parameters in reverse order to only store the last value.
+ * From the html form, the backend and the action are at the end.
+ */
+ while (cur_param > first_param) {
+ char *key, *value;
+
+ cur_param--;
+ if ((*cur_param == '&') || (cur_param == first_param)) {
+ /* Parse the key */
+ key = cur_param;
+ if (cur_param != first_param) {
+ /* delimit the string for the next loop */
+ *key++ = '\0';
+ }
+
+ /* Parse the value */
+ value = key;
+ while (*value != '\0' && *value != '=') {
+ value++;
+ }
+ if (*value == '=') {
+ /* Ok, a value is found, we can mark the end of the key */
+ *value++ = '\0';
+ }
+
+ /* Now we can check the key to see what to do */
+ if (!backend && strcmp(key, "b") == 0) {
+ backend = value;
+ }
+ else if (!action && strcmp(key, "action") == 0) {
+ if (strcmp(value, "disable") == 0) {
+ action = 1;
+ }
+ else if (strcmp(value, "enable") == 0) {
+ action = 2;
+ } else {
+ /* unknown action, no need to continue */
+ break;
+ }
+ }
+ else if (strcmp(key, "s") == 0) {
+ if (backend && action && get_backend_server(backend, value, &px, &sv)) {
+ switch (action) {
+ case 1:
+ if (! (sv->state & SRV_MAINTAIN)) {
+ /* Not already in maintenance, we can change the server state */
+ sv->state |= SRV_MAINTAIN;
+ set_server_down(sv);
+ s->data_ctx.stats.st_code = STAT_STATUS_DONE;
+ }
+ break;
+ case 2:
+ if ((sv->state & SRV_MAINTAIN)) {
+ /* Already in maintenance, we can change the server state */
+ set_server_up(sv);
+ s->data_ctx.stats.st_code = STAT_STATUS_DONE;
+ }
+ break;
+ }
+ }
+ }
+ next_param = cur_param;
+ }
+ }
+ return 0;
+}
+
/* This stream analyser runs all HTTP request processing which is common to
* frontends and backends, which means blocking ACLs, filters, connection-close,
* reqadd, stats and redirects. This is performed for the designated proxy.
@@ -3053,6 +3157,11 @@
* make it follow standard rules (eg: clear req->analysers).
*/
+ /* Was the status page requested with a POST ? */
+ if (txn->meth == HTTP_METH_POST) {
+ http_process_req_stat_post(s, req);
+ }
+
s->logs.tv_request = now;
s->data_source = DATA_SRC_STATS;
s->data_state = DATA_ST_INIT;
@@ -6943,10 +7052,10 @@
}
/*
- * In a GET or HEAD request, check if the requested URI matches the stats uri
+ * In a GET, HEAD or POST request, check if the requested URI matches the stats uri
* for the current backend.
*
- * It is assumed that the request is either a HEAD or GET and that the
+ * It is assumed that the request is either a HEAD, GET, or POST and that the
* t->be->uri_auth field is valid.
*
* Returns 1 if stats should be provided, otherwise 0.
@@ -6960,7 +7069,7 @@
if (!uri_auth)
return 0;
- if (txn->meth != HTTP_METH_GET && txn->meth != HTTP_METH_HEAD)
+ if (txn->meth != HTTP_METH_GET && txn->meth != HTTP_METH_HEAD && txn->meth != HTTP_METH_POST)
return 0;
memset(&t->data_ctx.stats, 0, sizeof(t->data_ctx.stats));
@@ -7004,6 +7113,24 @@
h++;
}
+ h = txn->req.sol + txn->req.sl.rq.u + uri_auth->uri_len;
+ while (h <= txn->req.sol + txn->req.sl.rq.u + txn->req.sl.rq.u_l - 8) {
+ if (memcmp(h, ";st=", 4) == 0) {
+ h += 4;
+
+ if (memcmp(h, STAT_STATUS_DONE, 4) == 0)
+ t->data_ctx.stats.st_code = STAT_STATUS_DONE;
+ else if (memcmp(h, STAT_STATUS_NONE, 4) == 0)
+ t->data_ctx.stats.st_code = STAT_STATUS_NONE;
+ else if (memcmp(h, STAT_STATUS_EXCD, 4) == 0)
+ t->data_ctx.stats.st_code = STAT_STATUS_EXCD;
+ else
+ t->data_ctx.stats.st_code = STAT_STATUS_UNKN;
+ break;
+ }
+ h++;
+ }
+
t->data_ctx.stats.flags |= STAT_SHOW_STAT | STAT_SHOW_INFO;
return 1;