pacemaker  2.1.7-0f7f88312f
Scalable High-Availability cluster resource manager
output_html.c
Go to the documentation of this file.
1 /*
2  * Copyright 2019-2022 the Pacemaker project contributors
3  *
4  * The version control history for this file may have further details.
5  *
6  * This source code is licensed under the GNU Lesser General Public License
7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8  */
9 
10 #include <crm_internal.h>
11 
12 #include <ctype.h>
13 #include <libxml/HTMLtree.h>
14 #include <stdarg.h>
15 #include <stdlib.h>
16 #include <stdio.h>
17 
19 #include <crm/common/xml.h>
20 
21 static const char *stylesheet_default =
22  ".bold { font-weight: bold }\n"
23 
24  ".online { color: green }\n"
25  ".offline { color: red }\n"
26  ".maint { color: blue }\n"
27  ".standby { color: blue }\n"
28  ".health_red { color: red }\n"
29  ".health_yellow { color: GoldenRod }\n"
30 
31  ".rsc-failed { color: red }\n"
32  ".rsc-failure-ignored { color: DarkGreen }\n"
33  ".rsc-managed { color: blue }\n"
34  ".rsc-multiple { color: orange }\n"
35  ".rsc-ok { color: green }\n"
36 
37  ".warning { color: red; font-weight: bold }";
38 
39 static gboolean cgi_output = FALSE;
40 static char *stylesheet_link = NULL;
41 static char *title = NULL;
42 static GSList *extra_headers = NULL;
43 
44 GOptionEntry pcmk__html_output_entries[] = {
45  { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output,
46  "Add CGI headers (requires --output-as=html)",
47  NULL },
48 
49  { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link,
50  "Link to an external stylesheet (requires --output-as=html)",
51  "URI" },
52 
53  { "html-title", 0, 0, G_OPTION_ARG_STRING, &title,
54  "Specify a page title (requires --output-as=html)",
55  "TITLE" },
56 
57  { NULL }
58 };
59 
60 /* The first several elements of this struct must be the same as the first
61  * several elements of private_data_s in lib/common/output_xml.c. This
62  * struct gets passed to a bunch of the pcmk__output_xml_* functions which
63  * assume an XML private_data_s. Keeping them laid out the same means this
64  * still works.
65  */
66 typedef struct private_data_s {
67  /* Begin members that must match the XML version */
68  xmlNode *root;
69  GQueue *parent_q;
70  GSList *errors;
71  /* End members that must match the XML version */
73 
74 static void
75 html_free_priv(pcmk__output_t *out) {
76  private_data_t *priv = NULL;
77 
78  if (out == NULL || out->priv == NULL) {
79  return;
80  }
81 
82  priv = out->priv;
83 
84  xmlFreeNode(priv->root);
85  g_queue_free(priv->parent_q);
86  g_slist_free(priv->errors);
87  free(priv);
88  out->priv = NULL;
89 }
90 
91 static bool
92 html_init(pcmk__output_t *out) {
93  private_data_t *priv = NULL;
94 
95  CRM_ASSERT(out != NULL);
96 
97  /* If html_init was previously called on this output struct, just return. */
98  if (out->priv != NULL) {
99  return true;
100  } else {
101  out->priv = calloc(1, sizeof(private_data_t));
102  if (out->priv == NULL) {
103  return false;
104  }
105 
106  priv = out->priv;
107  }
108 
109  priv->parent_q = g_queue_new();
110 
111  priv->root = create_xml_node(NULL, "html");
112  xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL);
113 
114  crm_xml_add(priv->root, "lang", "en");
115  g_queue_push_tail(priv->parent_q, priv->root);
116  priv->errors = NULL;
117 
118  pcmk__output_xml_create_parent(out, "body", NULL);
119 
120  return true;
121 }
122 
123 static void
124 add_error_node(gpointer data, gpointer user_data) {
125  char *str = (char *) data;
126  pcmk__output_t *out = (pcmk__output_t *) user_data;
127  out->list_item(out, NULL, "%s", str);
128 }
129 
130 static void
131 html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
132  private_data_t *priv = NULL;
133  htmlNodePtr head_node = NULL;
134  htmlNodePtr charset_node = NULL;
135 
136  CRM_ASSERT(out != NULL);
137 
138  priv = out->priv;
139 
140  /* If root is NULL, html_init failed and we are being called from pcmk__output_free
141  * in the pcmk__output_new path.
142  */
143  if (priv == NULL || priv->root == NULL) {
144  return;
145  }
146 
147  if (cgi_output && print) {
148  fprintf(out->dest, "Content-Type: text/html\n\n");
149  }
150 
151  /* Add the head node last - it's not needed earlier because it doesn't contain
152  * anything else that the user could add, and we want it done last to pick up
153  * any options that may have been given.
154  */
155  head_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) "head", NULL);
156 
157  if (title != NULL ) {
158  pcmk_create_xml_text_node(head_node, "title", title);
159  } else if (out->request != NULL) {
160  pcmk_create_xml_text_node(head_node, "title", out->request);
161  }
162 
163  charset_node = create_xml_node(head_node, "meta");
164  crm_xml_add(charset_node, "charset", "utf-8");
165 
166  /* Add any extra header nodes the caller might have created. */
167  for (int i = 0; i < g_slist_length(extra_headers); i++) {
168  xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1));
169  }
170 
171  /* Stylesheets are included two different ways. The first is via a built-in
172  * default (see the stylesheet_default const above). The second is via the
173  * html-stylesheet option, and this should obviously be a link to a
174  * stylesheet. The second can override the first. At least one should be
175  * given.
176  */
177  pcmk_create_xml_text_node(head_node, "style", stylesheet_default);
178 
179  if (stylesheet_link != NULL) {
180  htmlNodePtr link_node = create_xml_node(head_node, "link");
181  pcmk__xe_set_props(link_node, "rel", "stylesheet",
182  "href", stylesheet_link,
183  NULL);
184  }
185 
186  xmlAddPrevSibling(priv->root->children, head_node);
187 
188  if (g_slist_length(priv->errors) > 0) {
189  out->begin_list(out, "Errors", NULL, NULL);
190  g_slist_foreach(priv->errors, add_error_node, (gpointer) out);
191  out->end_list(out);
192  }
193 
194  if (print) {
195  htmlDocDump(out->dest, priv->root->doc);
196  }
197 
198  if (copy_dest != NULL) {
199  *copy_dest = copy_xml(priv->root);
200  }
201 
202  g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode);
203  extra_headers = NULL;
204 }
205 
206 static void
207 html_reset(pcmk__output_t *out) {
208  CRM_ASSERT(out != NULL);
209 
210  out->dest = freopen(NULL, "w", out->dest);
211  CRM_ASSERT(out->dest != NULL);
212 
213  html_free_priv(out);
214  html_init(out);
215 }
216 
217 static void
218 html_subprocess_output(pcmk__output_t *out, int exit_status,
219  const char *proc_stdout, const char *proc_stderr) {
220  char *rc_buf = NULL;
221 
222  CRM_ASSERT(out != NULL);
223 
224  rc_buf = crm_strdup_printf("Return code: %d", exit_status);
225 
226  pcmk__output_create_xml_text_node(out, "h2", "Command Output");
227  pcmk__output_create_html_node(out, "div", NULL, NULL, rc_buf);
228 
229  if (proc_stdout != NULL) {
230  pcmk__output_create_html_node(out, "div", NULL, NULL, "Stdout");
231  pcmk__output_create_html_node(out, "div", NULL, "output", proc_stdout);
232  }
233  if (proc_stderr != NULL) {
234  pcmk__output_create_html_node(out, "div", NULL, NULL, "Stderr");
235  pcmk__output_create_html_node(out, "div", NULL, "output", proc_stderr);
236  }
237 
238  free(rc_buf);
239 }
240 
241 static void
242 html_version(pcmk__output_t *out, bool extended) {
243  CRM_ASSERT(out != NULL);
244 
245  pcmk__output_create_xml_text_node(out, "h2", "Version Information");
246  pcmk__output_create_html_node(out, "div", NULL, NULL, "Program: Pacemaker");
247  pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Version: %s", PACEMAKER_VERSION));
248  pcmk__output_create_html_node(out, "div", NULL, NULL,
249  "Author: Andrew Beekhof and "
250  "the Pacemaker project contributors");
251  pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Build: %s", BUILD_VERSION));
252  pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Features: %s", CRM_FEATURES));
253 }
254 
255 G_GNUC_PRINTF(2, 3)
256 static void
257 html_err(pcmk__output_t *out, const char *format, ...) {
258  private_data_t *priv = NULL;
259  int len = 0;
260  char *buf = NULL;
261  va_list ap;
262 
263  CRM_ASSERT(out != NULL && out->priv != NULL);
264  priv = out->priv;
265 
266  va_start(ap, format);
267  len = vasprintf(&buf, format, ap);
268  CRM_ASSERT(len >= 0);
269  va_end(ap);
270 
271  priv->errors = g_slist_append(priv->errors, buf);
272 }
273 
274 G_GNUC_PRINTF(2, 3)
275 static int
276 html_info(pcmk__output_t *out, const char *format, ...) {
277  return pcmk_rc_no_output;
278 }
279 
280 static void
281 html_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
282  htmlNodePtr node = NULL;
283 
284  CRM_ASSERT(out != NULL);
285 
286  node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf);
287  crm_xml_add(node, "lang", "xml");
288 }
289 
290 G_GNUC_PRINTF(4, 5)
291 static void
292 html_begin_list(pcmk__output_t *out, const char *singular_noun,
293  const char *plural_noun, const char *format, ...) {
294  int q_len = 0;
295  private_data_t *priv = NULL;
296  xmlNodePtr node = NULL;
297 
298  CRM_ASSERT(out != NULL && out->priv != NULL);
299  priv = out->priv;
300 
301  /* If we are already in a list (the queue depth is always at least
302  * one because of the <html> element), first create a <li> element
303  * to hold the <h2> and the new list.
304  */
305  q_len = g_queue_get_length(priv->parent_q);
306  if (q_len > 2) {
307  pcmk__output_xml_create_parent(out, "li", NULL);
308  }
309 
310  if (format != NULL) {
311  va_list ap;
312  char *buf = NULL;
313  int len;
314 
315  va_start(ap, format);
316  len = vasprintf(&buf, format, ap);
317  va_end(ap);
318  CRM_ASSERT(len >= 0);
319 
320  if (q_len > 2) {
321  pcmk__output_create_xml_text_node(out, "h3", buf);
322  } else {
323  pcmk__output_create_xml_text_node(out, "h2", buf);
324  }
325 
326  free(buf);
327  }
328 
329  node = pcmk__output_xml_create_parent(out, "ul", NULL);
330  g_queue_push_tail(priv->parent_q, node);
331 }
332 
333 G_GNUC_PRINTF(3, 4)
334 static void
335 html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
336  htmlNodePtr item_node = NULL;
337  va_list ap;
338  char *buf = NULL;
339  int len;
340 
341  CRM_ASSERT(out != NULL);
342 
343  va_start(ap, format);
344  len = vasprintf(&buf, format, ap);
345  CRM_ASSERT(len >= 0);
346  va_end(ap);
347 
348  item_node = pcmk__output_create_xml_text_node(out, "li", buf);
349  free(buf);
350 
351  if (name != NULL) {
352  crm_xml_add(item_node, "class", name);
353  }
354 }
355 
356 static void
357 html_increment_list(pcmk__output_t *out) {
358  /* This function intentially left blank */
359 }
360 
361 static void
362 html_end_list(pcmk__output_t *out) {
363  private_data_t *priv = NULL;
364 
365  CRM_ASSERT(out != NULL && out->priv != NULL);
366  priv = out->priv;
367 
368  /* Remove the <ul> tag. */
369  g_queue_pop_tail(priv->parent_q);
371 
372  /* Remove the <li> created for nested lists. */
373  if (g_queue_get_length(priv->parent_q) > 2) {
375  }
376 }
377 
378 static bool
379 html_is_quiet(pcmk__output_t *out) {
380  return false;
381 }
382 
383 static void
384 html_spacer(pcmk__output_t *out) {
385  CRM_ASSERT(out != NULL);
386  pcmk__output_create_xml_node(out, "br", NULL);
387 }
388 
389 static void
390 html_progress(pcmk__output_t *out, bool end) {
391  /* This function intentially left blank */
392 }
393 
395 pcmk__mk_html_output(char **argv) {
396  pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
397 
398  if (retval == NULL) {
399  return NULL;
400  }
401 
402  retval->fmt_name = "html";
403  retval->request = pcmk__quote_cmdline(argv);
404 
405  retval->init = html_init;
406  retval->free_priv = html_free_priv;
407  retval->finish = html_finish;
408  retval->reset = html_reset;
409 
411  retval->message = pcmk__call_message;
412 
413  retval->subprocess_output = html_subprocess_output;
414  retval->version = html_version;
415  retval->info = html_info;
416  retval->transient = html_info;
417  retval->err = html_err;
418  retval->output_xml = html_output_xml;
419 
420  retval->begin_list = html_begin_list;
421  retval->list_item = html_list_item;
422  retval->increment_list = html_increment_list;
423  retval->end_list = html_end_list;
424 
425  retval->is_quiet = html_is_quiet;
426  retval->spacer = html_spacer;
427  retval->progress = html_progress;
428  retval->prompt = pcmk__text_prompt;
429 
430  return retval;
431 }
432 
433 xmlNodePtr
434 pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id,
435  const char *class_name, const char *text) {
436  htmlNodePtr node = NULL;
437 
438  CRM_ASSERT(out != NULL);
439  CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL);
440 
441  node = pcmk__output_create_xml_text_node(out, element_name, text);
442 
443  if (class_name != NULL) {
444  crm_xml_add(node, "class", class_name);
445  }
446 
447  if (id != NULL) {
448  crm_xml_add(node, "id", id);
449  }
450 
451  return node;
452 }
453 
454 void
455 pcmk__html_add_header(const char *name, ...) {
456  htmlNodePtr header_node;
457  va_list ap;
458 
459  va_start(ap, name);
460 
461  header_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) name, NULL);
462  while (1) {
463  char *key = va_arg(ap, char *);
464  char *value;
465 
466  if (key == NULL) {
467  break;
468  }
469 
470  value = va_arg(ap, char *);
471  crm_xml_add(header_node, key, value);
472  }
473 
474  extra_headers = g_slist_append(extra_headers, header_node);
475 
476  va_end(ap);
477 }
void(* end_list)(pcmk__output_t *out)
#define CRM_CHECK(expr, failure_action)
Definition: logging.h:238
char data[0]
Definition: cpg.c:55
void void void void void pcmk__text_prompt(const char *prompt, bool echo, char **dest)
Definition: output_text.c:402
const char * name
Definition: cib.c:26
int(* message)(pcmk__output_t *out, const char *message_id,...)
const char * fmt_name
The name of this output formatter.
bool(* is_quiet)(pcmk__output_t *out)
void(* spacer)(pcmk__output_t *out)
xmlNodePtr pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name,...) G_GNUC_NULL_TERMINATED
Definition: output_xml.c:442
const char * crm_xml_add(xmlNode *node, const char *name, const char *value)
Create an XML attribute with specified name and value.
Definition: nvpair.c:302
void pcmk__register_message(pcmk__output_t *out, const char *message_id, pcmk__message_fn_t fn)
Definition: output.c:188
enum crm_exit_e crm_exit_t
int(* info)(pcmk__output_t *out, const char *format,...) G_GNUC_PRINTF(2
#define PACEMAKER_VERSION
Definition: config.h:517
int pcmk__call_message(pcmk__output_t *out, const char *message_id,...)
Definition: output.c:166
void(* prompt)(const char *prompt, bool echo, char **dest)
GOptionEntry pcmk__html_output_entries[]
Definition: output_html.c:44
void * priv
Implementation-specific private data.
xmlNode * copy_xml(xmlNode *src_node)
Definition: xml.c:789
void pcmk__html_add_header(const char *name,...)
Definition: output_html.c:455
void(* register_message)(pcmk__output_t *out, const char *message_id, pcmk__message_fn_t fn)
xmlNode * pcmk_create_xml_text_node(xmlNode *parent, const char *name, const char *content)
Definition: xml.c:672
#define BUILD_VERSION
Definition: config.h:8
pcmk__output_t * pcmk__mk_html_output(char **argv)
Definition: output_html.c:395
struct private_data_s private_data_t
void(* free_priv)(pcmk__output_t *out)
char * crm_strdup_printf(char const *format,...) G_GNUC_PRINTF(1
bool(* init)(pcmk__output_t *out)
int(*) int(*) void(*) void(* output_xml)(pcmk__output_t *out, const char *name, const char *buf)
int(*) int(*) void(* err)(pcmk__output_t *out, const char *format,...) G_GNUC_PRINTF(2
Wrappers for and extensions to libxml2.
void(* finish)(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest)
xmlNode * create_xml_node(xmlNode *parent, const char *name)
Definition: xml.c:638
xmlNodePtr pcmk__output_create_xml_node(pcmk__output_t *out, const char *name,...) G_GNUC_NULL_TERMINATED
Definition: output_xml.c:478
int(*) int(* transient)(pcmk__output_t *out, const char *format,...) G_GNUC_PRINTF(2
void pcmk__output_xml_pop_parent(pcmk__output_t *out)
Definition: output_xml.c:522
void pcmk__xe_set_props(xmlNodePtr node,...) G_GNUC_NULL_TERMINATED
Definition: xml.c:2654
FILE * dest
Where output should be written.
const xmlChar * pcmkXmlStr
Definition: xml.h:50
#define CRM_ASSERT(expr)
Definition: results.h:42
void(*) void(* list_item)(pcmk__output_t *out, const char *name, const char *format,...) G_GNUC_PRINTF(3
xmlNodePtr pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id, const char *class_name, const char *text)
Definition: output_html.c:434
gchar * request
A copy of the request that generated this output.
This structure contains everything that makes up a single output formatter.
void(* version)(pcmk__output_t *out, bool extended)
void(* begin_list)(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, const char *format,...) G_GNUC_PRINTF(4
void(* reset)(pcmk__output_t *out)
void(* progress)(pcmk__output_t *out, bool end)
#define CRM_FEATURES
Definition: config.h:33
gchar * pcmk__quote_cmdline(gchar **argv)
Definition: cmdline.c:163
xmlNodePtr pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content)
Definition: output_xml.c:497
void(*) void(*) void(* increment_list)(pcmk__output_t *out)
void(* subprocess_output)(pcmk__output_t *out, int exit_status, const char *proc_stdout, const char *proc_stderr)