1 /*
2  ex: set tabstop=4 shiftwidth=4 autoindent:
3  +-------------------------------------------------------------------------+
4  | Copyright (C) 2004-2021 The Cacti Group                                 |
5  |                                                                         |
6  | This program is free software; you can redistribute it and/or           |
7  | modify it under the terms of the GNU Lesser General Public              |
8  | License as published by the Free Software Foundation; either            |
9  | version 2.1 of the License, or (at your option) any later version. 	   |
10  |                                                                         |
11  | This program is distributed in the hope that it will be useful,         |
12  | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
13  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
14  | GNU Lesser General Public License for more details.                     |
15  |                                                                         |
16  | You should have received a copy of the GNU Lesser General Public        |
17  | License along with this library; if not, write to the Free Software     |
18  | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA           |
19  | 02110-1301, USA                                                         |
20  |                                                                         |
21  +-------------------------------------------------------------------------+
22  | spine: a backend data gatherer for cacti                                |
23  +-------------------------------------------------------------------------+
24  | This poller would not have been possible without:                       |
25  |   - Larry Adams (current development and enhancements)                  |
26  |   - Rivo Nurges (rrd support, mysql poller cache, misc functions)       |
27  |   - RTG (core poller code, pthreads, snmp, autoconf examples)           |
28  |   - Brady Alleman/Doug Warner (threading ideas, implimentation details) |
29  +-------------------------------------------------------------------------+
30  | - Cacti - http://www.cacti.net/                                         |
31  +-------------------------------------------------------------------------+
32  *
33  * COMMAND-LINE PARAMETERS
34  *
35  * -h | --help
36  * -v | --version
37  *
38  *	Show a brief help listing, then exit.
39  *
40  * -C | --conf=F
41  *
42  *	Provide the name of the Spine configuration file, which contains
43  *	the parameters for connecting to the database. In the absence of
44  *	this, it looks [WHERE?]
45  *
46  * -f | --first=ID
47  *
48  *	Start polling with device <ID> (else starts at the beginning)
49  *
50  * -l | --last=ID
51  *
52  *	Stop polling after device <ID> (else ends with the last one)
53  *
54  * -m | --mibs
55  *
56  *	Collect all system mibs this pass
57  *
58  * -N | --mode=online|offline|recovery
59  *
60  *	For remote pollers, the polling mode.  The default is 'online'
61  *
62  * -H | --hostlist="hostid1,hostid2,hostid3,...,hostidn"
63  *
64  *	Override the expected first host, last host behavior with a list of hostids.
65  *
66  * -O | --option=setting:value
67  *
68  *	Override a DB-provided value from the settings table in the DB.
69  *
70  * -C | -conf=FILE
71  *
72  *	Specify the location of the Spine configuration file.
73  *
74  * -R | --readonly
75  *
76  *	This processing is readonly with respect to the database: it's
77  *	meant only for developer testing.
78  *
79  * -S | --stdout
80  *
81  *	All logging goes to the standard output
82  *
83  * -V | --verbosity=V
84  *
85  * Set the debug logging verbosity to <V>. Can be 1..5 or
86  *	NONE/LOW/MEDIUM/HIGH/DEBUG (case insensitive).
87  *
88  * The First/Last device IDs are all relative to the "hosts" table in the
89  * Cacti database, and this mechanism allows us to split up the polling
90  * duties across multiple "spine" instances: each one gets a subset of
91  * the polling range.
92  *
93  * For compatibility with poller.php, we also accept the first and last
94  * device IDs as standalone parameters on the command line.
95 */
96 
97 #include "common.h"
98 #include "spine.h"
99 
100 /* Global Variables */
101 int entries = 0;
102 int num_hosts = 0;
103 int pending_threads = 0;
104 sem_t active_threads;
105 sem_t active_scripts;
106 double start_time;
107 
108 config_t set;
109 php_t	*php_processes = 0;
110 char	config_paths[CONFIG_PATHS][BUFSIZE];
111 int     *debug_devices;
112 
113 pool_t  *db_pool_local;
114 pool_t  *db_pool_remote;
115 
116 static char *getarg(char *opt, char ***pargv);
117 static void display_help(int only_version);
118 void poller_push_data_to_main();
119 
120 #ifdef HAVE_LCAP
121 /* This patch is adapted (copied) patch for ntpd from Jarno Huuskonen and
122  * Pekka Savola that was adapted (copied) from a patch by Chris Wings to drop
123  * root for xntpd.
124  */
drop_root(uid_t server_uid,gid_t server_gid)125 void drop_root(uid_t server_uid, gid_t server_gid) {
126 	cap_t caps;
127 	if (prctl(PR_SET_KEEPCAPS, 1)) {
128 		SPINE_LOG_HIGH(("prctl(PR_SET_KEEPCAPS, 1) failed"));
129 		exit(1);
130 	}
131 
132 	if (setgroups(0, NULL) == -1) {
133 		SPINE_LOG_HIGH(("setgroups failed."));
134 		exit(1);
135 	}
136 
137 	if (setegid(server_gid) == -1 || seteuid(server_uid) == -1) {
138 		SPINE_LOG_HIGH(("setegid/seteuid to uid=%d/gid=%d failed.", server_uid, server_gid));
139 		exit(1);
140 	}
141 
142 	caps = cap_from_text("cap_net_raw=eip");
143 	if (caps == NULL) {
144 		SPINE_LOG_HIGH(("cap_from_text failed."));
145 		exit(1);
146 	}
147 
148 	if (cap_set_proc(caps) == -1) {
149 		SPINE_LOG_HIGH(("cap_set_proc failed."));
150 		exit(1);
151 	}
152 
153 	/* Try to free the memory from cap_from_text */
154 	cap_free( caps );
155 
156 	if ( setregid(server_gid, server_gid) == -1 ||
157 		setreuid(server_uid, server_uid) == -1 ) {
158 		SPINE_LOG_HIGH(("setregid/setreuid to uid=%d/gid=%d failed.",
159 			server_uid, server_gid));
160 		exit(1);
161 	}
162 
163 	SPINE_LOG_LOW(("running as uid(%d)/gid(%d) euid(%d)/egid(%d) with cap_net_raw=eip.",
164 		getuid(), getgid(), geteuid(), getegid()));
165 }
166 #endif /* HAVE_LCAP */
167 
168 /*! \fn main(int argc, char *argv[])
169  *  \brief The Spine program entry point
170  *  \param argc The number of arguments passed to the function plus one (+1)
171  *  \param argv An array of the command line arguments
172  *
173  *  The Spine entry point.  This function performs the following tasks.
174  *  1) Processes command line input parameters
175  *  2) Processes the Spine configuration file to obtain database access information
176  *  3) Process runtime parameters from the settings table
177  *  4) Initialize the runtime threads and mutexes for the threaded environment
178  *  5) Initialize Net-SNMP, MySQL, and the PHP Script Server (if required)
179  *  6) Spawns X threads in order to process hosts
180  *  7) Loop until either all hosts have been processed or until the poller runtime
181  *     has been exceeded
182  *  8) Close database and free variables
183  *  9) Log poller process statistics if required
184  *  10) Exit
185  *
186  *  Note: Command line runtime parameters override any database settings.
187  *
188  *  \return 0 if SUCCESS, or -1 if FAILED
189  *
190  */
main(int argc,char * argv[])191 int main(int argc, char *argv[]) {
192 	struct timeval now;
193 	char *conf_file = NULL;
194 	double begin_time, end_time, cur_time;
195 	int num_rows = 0;
196 	int device_counter = 0;
197 	int valid_conf_file = FALSE;
198 	char querybuf[MEGA_BUFSIZE], *qp = querybuf;
199 	char *host_time = NULL;
200 	double host_time_double = 0;
201 	int itemsPT = 0;
202 	int device_threads;
203 	sem_t thread_init_sem;
204 	int a_threads_value;
205 	struct timespec until_spec;
206 
207 	gettimeofday(&now, NULL);
208 	start_time = TIMEVAL_TO_DOUBLE(now);
209 
210 	#ifdef HAVE_LCAP
211 	if (geteuid() == 0) {
212 		drop_root(getuid(), getgid());
213 	}
214 	#endif /* HAVE_LCAP */
215 
216 	pthread_t* threads = NULL;
217 	poller_thread_t** details = NULL;
218 	poller_thread_t* poller_details = NULL;
219 	pthread_attr_t attr;
220 
221 	int* ids = NULL;
222 	int mode = REMOTE;
223 	MYSQL mysql;
224 	MYSQL mysqlr;
225 	MYSQL_RES *result  = NULL;
226 	MYSQL_RES *tresult = NULL;
227 	MYSQL_ROW mysql_row;
228 	int canexit = FALSE;
229 	int host_id = 0;
230 	int i;
231 	int thread_status = 0;
232 	int total_items   = 0;
233 	int change_host   = TRUE;
234 	int current_thread;
235 	int threads_final = 0;
236 	int threads_missing = -1;
237 	int threads_count;
238 
239 	UNUSED_PARAMETER(argc);		/* we operate strictly with argv */
240 
241 	/* install the spine signal handler */
242 	install_spine_signal_handler();
243 
244 	/* establish php processes and initialize space */
245 	php_processes = (php_t*) calloc(MAX_PHP_SERVERS, sizeof(php_t));
246 	for (i = 0; i < MAX_PHP_SERVERS; i++) {
247 		php_processes[i].php_state = PHP_BUSY;
248 	}
249 
250 	/* create the array of debug devices */
251 	debug_devices = calloc(100, sizeof(int));
252 
253 	/* initialize icmp_avail */
254 	set.icmp_avail = TRUE;
255 
256 	/* detect and compensate for stdin/stderr ttys */
257 	if (!isatty(fileno(stdout))) {
258 		set.stdout_notty = TRUE;
259 	} else {
260 		set.stdout_notty = FALSE;
261 	}
262 
263 	if (!isatty(fileno(stderr))) {
264 		set.stderr_notty = TRUE;
265 	} else {
266 		set.stderr_notty = FALSE;
267 	}
268 
269 	/* set start time for cacti */
270 	begin_time = get_time_as_double();
271 
272 	/* set default verbosity */
273 	set.log_level = POLLER_VERBOSITY_LOW;
274 
275 	/* set default log separator */
276 	set.log_datetime_separator = GDC_DEFAULT;
277 
278 	/* set default log format */
279 	set.log_datetime_format = GD_DEFAULT;
280 
281 	/* set the default exit code */
282 	set.exit_code = 0;
283 	set.exit_size = 0;
284 
285 	/* get static defaults for system */
286 	config_defaults();
287 
288 	/*! ----------------------------------------------------------------
289 	 * PROCESS COMMAND LINE
290 	 *
291 	 * Run through the list of ARGV words looking for parameters we
292 	 * know about. Most have two flavors (-C + --conf), and many
293 	 * themselves take a parameter.
294 	 *
295 	 * These parameters can be structured in two ways:
296 	 *
297 	 *	--conf=FILE		both parts in one argv[] string
298 	 *	--conf FILE		two separate argv[] strings
299 	 *
300 	 * We set "arg" to point to "--conf", and "opt" to point to FILE.
301 	 * The helper routine
302 	 *
303 	 * In each loop we set "arg" to next argv[] string, then look
304 	 * to see if it has an equal sign. If so, we split it in half
305 	 * and point to the option separately.
306 	 *
307 	 * NOTE: most direction to the program is given with dash-type
308 	 * parameters, but we also allow standalone numeric device IDs
309 	 * in "first last" format: this is how poller.php calls this
310 	 * program.
311 	 */
312 
313 	/* initialize some global variables */
314 	set.poller_id         = 1;
315 	set.start_host_id     = -1;
316 	set.end_host_id       = -1;
317 	set.host_id_list[0]   = '\0';
318 	set.php_initialized   = FALSE;
319 	set.logfile_processed = FALSE;
320 	set.parent_fork       = SPINE_PARENT;
321 	set.mode              = REMOTE_ONLINE;
322 
323 	for (argv++; *argv; argv++) {
324 		char	*arg = *argv;
325 		char	*opt = strchr(arg, '=');	/* pick off the =VALUE part */
326 
327 		if (opt) *opt++ = '\0';
328 
329 		if (STRMATCH(arg, "-f") || STRMATCH(arg, "--first")) {
330 			if (HOSTID_DEFINED(set.start_host_id)) {
331 				die("ERROR: %s can only be used once", arg);
332 			}
333 
334 			set.start_host_id = atoi(opt = getarg(opt, &argv));
335 
336 			if (!HOSTID_DEFINED(set.start_host_id)) {
337 				die("ERROR: '%s=%s' is invalid first-host ID", arg, opt);
338 			}
339 		}
340 
341 		else if (STRMATCH(arg, "-l") || STRIMATCH(arg, "--last")) {
342 			if (HOSTID_DEFINED(set.end_host_id)) {
343 				die("ERROR: %s can only be used once", arg);
344 			}
345 
346 			set.end_host_id = atoi(opt = getarg(opt, &argv));
347 
348 			if (!HOSTID_DEFINED(set.end_host_id)) {
349 				die("ERROR: '%s=%s' is invalid last-host ID", arg, opt);
350 			}
351 		}
352 
353 		else if (STRMATCH(arg, "-p") || STRIMATCH(arg, "--poller")) {
354 			set.poller_id = atoi(getarg(opt, &argv));
355 		}
356 
357 		else if (STRMATCH(arg, "-N") || STRIMATCH(arg, "--mode")) {
358 			if (STRIMATCH(getarg(opt, &argv), "online")) {
359 				set.mode = REMOTE_ONLINE;
360 			} else if (STRIMATCH(getarg(opt, &argv), "offline")) {
361 				set.mode = REMOTE_OFFLINE;
362 			} else if (STRIMATCH(getarg(opt, &argv), "recovery")) {
363 				set.mode = REMOTE_RECOVERY;
364 			} else {
365 				die("ERROR: invalid polling mode '%s' specified", opt);
366 			}
367 		}
368 
369 		else if (STRMATCH(arg, "-H") || STRIMATCH(arg, "--hostlist")) {
370 			snprintf(set.host_id_list, BIG_BUFSIZE, "%s", getarg(opt, &argv));
371 		}
372 
373 		else if (STRMATCH(arg, "-M") || STRMATCH(arg, "--mibs")) {
374 			set.mibs = 1;
375 		}
376 
377 		else if (STRMATCH(arg, "-h") || STRMATCH(arg, "--help")) {
378 			display_help(FALSE);
379 
380 			exit(EXIT_SUCCESS);
381 		}
382 
383 		else if (STRMATCH(arg, "-v") || STRMATCH(arg, "--version")) {
384 			display_help(TRUE);
385 
386 			exit(EXIT_SUCCESS);
387 		}
388 
389 		else if (STRMATCH(arg, "-O") || STRIMATCH(arg, "--option")) {
390 			char *setting = getarg(opt, &argv);
391 			char *value   = strchr(setting, ':');
392 
393 			if (*value) {
394 				*value++ = '\0';
395 			} else {
396 				die("ERROR: -O requires setting:value");
397 			}
398 
399 			set_option(setting, value);
400 		}
401 
402 		else if (STRMATCH(arg, "-R") || STRMATCH(arg, "--readonly") || STRMATCH(arg, "--read-only")) {
403 			set.SQL_readonly = TRUE;
404 		}
405 
406 		else if (STRMATCH(arg, "-C") || STRMATCH(arg, "--conf")) {
407 			conf_file = strdup(getarg(opt, &argv));
408 		}
409 
410 		else if (STRMATCH(arg, "-S") || STRMATCH(arg, "--stdout")) {
411 			set_option("log_destination", "STDOUT");
412 		}
413 
414 		else if (STRMATCH(arg, "-L") || STRMATCH(arg, "--log")) {
415 			set_option("log_destination", getarg(opt, &argv));
416 		}
417 
418 		else if (STRMATCH(arg, "-V") || STRMATCH(arg, "--verbosity")) {
419 			set_option("log_verbosity", getarg(opt, &argv));
420 		}
421 
422 		else if (!HOSTID_DEFINED(set.start_host_id) && all_digits(arg)) {
423 			set.start_host_id = atoi(arg);
424 		}
425 
426 		else if (!HOSTID_DEFINED(set.end_host_id) && all_digits(arg)) {
427 			set.end_host_id = atoi(arg);
428 		}
429 
430 		else {
431 			die("ERROR: %s is an unknown command-line parameter", arg);
432 		}
433 	}
434 
435 	/* we attempt to support scripts better in cygwin */
436 	#if defined(__CYGWIN__)
437 	setenv("CYGWIN", "nodosfilewarning", 1);
438 	if (file_exists("./sh.exe")) {
439 		set.cygwinshloc = 0;
440 		if (set.log_level == POLLER_VERBOSITY_DEBUG) {
441 			printf("The Shell Command Exists in the current directory\n");
442 		}
443 	} else {
444 		set.cygwinshloc = 1;
445 		if (set.log_level == POLLER_VERBOSITY_DEBUG) {
446 			printf("The Shell Command Exists in the /bin directory\n");
447 		}
448 	}
449 	#endif
450 
451 	/* we require either both the first and last hosts, or niether host */
452 	if ((HOSTID_DEFINED(set.start_host_id) != HOSTID_DEFINED(set.end_host_id)) &&
453 		(!strlen(set.host_id_list))) {
454 		die("ERROR: must provide both -f/-l, a hostlist (-H/--hostlist), or neither");
455 	}
456 
457 	if (set.start_host_id > set.end_host_id) {
458 		die("ERROR: Invalid row spec; first host_id must be less than the second");
459 	}
460 
461 	/* read configuration file to establish local environment */
462 	if (conf_file) {
463 		if ((read_spine_config(conf_file)) < 0) {
464 			die("ERROR: Could not read config file: %s", conf_file);
465 		} else {
466 			valid_conf_file = TRUE;
467 		}
468 	} else {
469 		if (!(conf_file = calloc(CONFIG_PATHS, DBL_BUFSIZE))) {
470 			die("ERROR: Fatal malloc error: spine.c conf_file!");
471 		}
472 
473 		for (i=0; i<CONFIG_PATHS; i++) {
474 			snprintf(conf_file, DBL_BUFSIZE, "%s%s", config_paths[i], DEFAULT_CONF_FILE);
475 
476 			if (read_spine_config(conf_file) >= 0) {
477 				valid_conf_file = TRUE;
478 				break;
479 			}
480 
481 			if (i == CONFIG_PATHS-1) {
482 				snprintf(conf_file, DBL_BUFSIZE, "%s%s", config_paths[0], DEFAULT_CONF_FILE);
483 			}
484 		}
485 	}
486 
487 	if (valid_conf_file) {
488 		/* read settings table from the database to further establish environment */
489 		read_config_options();
490 	} else {
491 		die("FATAL: Unable to read configuration file!");
492 	}
493 
494 	/* set the poller interval for those who use less than 5 minute intervals */
495 	if (set.poller_interval == 0) {
496 		set.poller_interval = 300;
497 	}
498 
499 	/* tokenize the debug devices */
500 	if (strlen(set.selective_device_debug)) {
501 		SPINE_LOG_DEBUG(("DEBUG: Selective Debug Devices %s", set.selective_device_debug));
502 		int i = 0;
503 		char *token = strtok(set.selective_device_debug, ",");
504 		while(token) {
505 			debug_devices[i]   = atoi(token);
506 			debug_devices[i+1] = '\0';
507 			token = strtok(NULL, ",");
508 			i++;
509 		}
510 	} else {
511 		debug_devices[0] = '\0';
512 	}
513 
514 	/* connect for main loop */
515 	db_connect(LOCAL, &mysql);
516 
517 	/* setup local connection pool for hosts */
518 	db_pool_local = (pool_t *) calloc(set.threads, sizeof(pool_t));
519 	db_create_connection_pool(LOCAL);
520 
521 	if (set.poller_id > 1 && set.mode == REMOTE_ONLINE) {
522 		db_connect(REMOTE, &mysqlr);
523 		mode = REMOTE;
524 
525 		/* setup remote connection pool for hosts */
526 		db_pool_remote = (pool_t *) calloc(set.threads, sizeof(pool_t));
527 		db_create_connection_pool(REMOTE);
528 	} else {
529 		mode = LOCAL;
530 	}
531 
532 	/* Since MySQL 5.7 the sql_mode defaults are too strict for cacti */
533 	db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE', ''))");
534 	db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))");
535 
536 	if (set.log_level == POLLER_VERBOSITY_DEBUG) {
537 		SPINE_LOG_DEBUG(("DEBUG: Version %s starting", VERSION));
538 
539 		if (set.poller_id > 1) {
540 			if (mode == REMOTE) {
541 				SPINE_LOG_DEBUG(("DEBUG: Sending entries to remote database in 'online' mode"));
542 			} else {
543 				SPINE_LOG_DEBUG(("DEBUG: Sending entries to local database in 'offline', or 'recovery' mode"));
544 			}
545 		}
546 	} else {
547 		if (!set.stdout_notty) {
548 			printf("Version %s starting\n", VERSION);
549 
550 			if (set.poller_id > 1) {
551 				if (mode == REMOTE) {
552 					printf("Sending entries to remote database in 'online' mode\n");
553 				} else {
554 					printf("Sending entries to local database in 'offline', or 'recovery' mode\n");
555 				}
556 			}
557 		}
558 	}
559 
560 	/* see if mysql is thread safe */
561 	if (mysql_thread_safe()) {
562 		if (set.log_level == POLLER_VERBOSITY_DEBUG) {
563 			SPINE_LOG(("DEBUG: MySQL is Thread Safe!"));
564 		}
565 	} else {
566 		SPINE_LOG(("WARNING: MySQL is NOT Thread Safe!"));
567 	}
568 
569 	/* test for asroot permissions for ICMP */
570 	checkAsRoot();
571 
572 	/* initialize SNMP */
573 	SPINE_LOG_DEBUG(("DEBUG: Initializing Net-SNMP API"));
574 	snmp_spine_init();
575 
576 	/* initialize PHP if required */
577 	SPINE_LOG_DEBUG(("DEBUG: Initializing PHP Script Server(s)"));
578 
579 	/* tell spine that it is parent, and set the poller id */
580 	set.parent_fork = SPINE_PARENT;
581 
582 	/* initialize the script server */
583 	if (set.php_required) {
584 		php_init(PHP_INIT);
585 		set.php_initialized    = TRUE;
586 		set.php_current_server = 0;
587 	}
588 
589 	/* determine if the poller_id field exists in the host table */
590 	result = db_query(&mysql, LOCAL, "SHOW COLUMNS FROM host LIKE 'poller_id'");
591 	if (mysql_num_rows(result)) {
592 		set.poller_id_exists = TRUE;
593 	} else {
594 		set.poller_id_exists = FALSE;
595 
596 		if (set.poller_id > 0) {
597 			SPINE_LOG(("WARNING: PollerID > 0, but 'host' table does NOT contain the poller_id column!!"));
598 		}
599 	}
600 	db_free_result(result);
601 
602 	/* determine if the device_threads field exists in the host table */
603 	result = db_query(&mysql, LOCAL, "SHOW COLUMNS FROM host LIKE 'device_threads'");
604 	if (mysql_num_rows(result)) {
605 		set.device_threads_exists = TRUE;
606 	} else {
607 		set.device_threads_exists = FALSE;
608 	}
609 	db_free_result(result);
610 
611 	if (set.device_threads_exists) {
612 		SPINE_LOG_MEDIUM(("Spine will support multithread device polling."));
613 	} else {
614 		SPINE_LOG_MEDIUM(("Spine did not detect multithreaded device polling."));
615 	}
616 
617 	/* obtain the list of hosts to poll */
618 	if (set.device_threads_exists) {
619 		qp += sprintf(qp, "SELECT id, device_threads FROM host");
620 	} else {
621 		qp += sprintf(qp, "SELECT id, '1' as device_threads FROM host");
622 	}
623 	qp += sprintf(qp, " WHERE disabled=''");
624 	if (!strlen(set.host_id_list)) {
625 		qp += append_hostrange(qp, "id");	/* AND id BETWEEN a AND b */
626 	} else {
627 		qp += sprintf(qp, " AND id IN(%s)", set.host_id_list);
628 	}
629 	if (set.poller_id_exists) {
630 		qp += sprintf(qp, " AND host.poller_id=%i", set.poller_id);
631 	}
632 	qp += sprintf(qp, " ORDER BY polling_time DESC");
633 
634 	result = db_query(&mysql, LOCAL, querybuf);
635 
636 	if (set.poller_id == 1) {
637 		num_rows = mysql_num_rows(result) + 1; /* pollerid 1 takes care of not host based data sources */
638 	} else {
639 		num_rows = mysql_num_rows(result);
640 	}
641 
642 	if (!(threads = (pthread_t *)malloc(num_rows * sizeof(pthread_t)))) {
643 		die("ERROR: Fatal malloc error: spine.c threads!");
644 	}
645 
646 	if (!(details = (poller_thread_t **)malloc(num_rows * sizeof(poller_thread_t*)))) {
647 		die("ERROR: Fatal malloc error: spine.c details!");
648 	}
649 
650 	if (!(ids = (int *)malloc(num_rows * sizeof(int)))) {
651 		die("ERROR: Fatal malloc error: spine.c host id's!");
652 	}
653 
654 	/* initialize winsock library on Windows */
655 	SOCK_STARTUP;
656 
657 	/* mark the spine process as started */
658 	snprintf(querybuf, BIG_BUFSIZE, "INSERT INTO poller_time (poller_id, pid, start_time, end_time) VALUES (%i, %i, NOW(), '0000-00-00 00:00:00')", set.poller_id, getpid());
659 	if (mode == REMOTE) {
660 		db_insert(&mysqlr, REMOTE, querybuf);
661 	} else {
662 		db_insert(&mysql, LOCAL, querybuf);
663 	}
664 
665 	/* initialize threads and mutexes */
666 	pthread_attr_init(&attr);
667 	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
668 
669 	init_mutexes();
670 
671 	/* initialize active_threads semaphore */
672 	sem_init(&active_threads, 0, set.threads);
673 
674 	/* initialize active_scripts semaphore */
675 	sem_init(&active_scripts, 0, MAX_SIMULTANEOUS_SCRIPTS);
676 
677 	/* initialize thread initialization semaphore */
678 	sem_init(&thread_init_sem, 0, 1);
679 
680 	/* specify the point of timeout for timedwait semaphores */
681 	until_spec.tv_sec = (time_t)(set.poller_interval + begin_time - 0.2);
682 	until_spec.tv_nsec = 0;
683 
684 	sem_getvalue(&active_threads, &a_threads_value);
685 	SPINE_LOG_MEDIUM(("DEBUG: Initial Value of Active Threads is %i", set.threads - a_threads_value));
686 
687 	/* tell fork processes that they are now active */
688 	set.parent_fork = SPINE_FORK;
689 
690 	/* initialize the threading code */
691 	device_threads   = 1;
692 	current_thread   = 0;
693 
694 	/* poller 0 always polls host 0 */
695 	if (set.poller_id == 1) {
696 		host_id     = 0;
697 		change_host = FALSE;
698 	} else {
699 		change_host = TRUE;
700 	}
701 
702 	/* loop through devices until done */
703 	while (canexit == FALSE && device_counter < num_rows) {
704 		if (change_host) {
705 			mysql_row       = mysql_fetch_row(result);
706 			host_id         = atoi(mysql_row[0]);
707 			device_threads  = atoi(mysql_row[1]);
708 			current_thread  = 1;
709 
710 			if (device_threads < 1) {
711 				device_threads = 1;
712 			}
713 		} else {
714 			current_thread++;
715 		}
716 
717 		/* adjust device threads in cases where the host does not have sufficient data sources */
718 		snprintf(querybuf, BIG_BUFSIZE, "SELECT COUNT(local_data_id) FROM poller_item WHERE host_id=%i AND rrd_next_step <=0", host_id);
719 		tresult   = db_query(&mysql, LOCAL, querybuf);
720 		mysql_row = mysql_fetch_row(tresult);
721 
722 		total_items = atoi(mysql_row[0]);
723 		db_free_result(tresult);
724 
725 		if (total_items && total_items < device_threads) {
726 			device_threads = total_items;
727 		}
728 
729 		change_host = (current_thread >= device_threads) ? TRUE : FALSE;
730 
731 		/* determine how many items will be polled per thread */
732 		if (device_threads > 1) {
733 			if (current_thread == 1) {
734 				snprintf(querybuf, BIG_BUFSIZE, "SELECT CEIL(COUNT(local_data_id)/%i) FROM poller_item WHERE host_id=%i AND rrd_next_step <=0", device_threads, host_id);
735 				tresult   = db_query(&mysql, LOCAL, querybuf);
736 				mysql_row = mysql_fetch_row(tresult);
737 
738 				itemsPT   = atoi(mysql_row[0]);
739 				db_free_result(tresult);
740 
741 				if (host_time) free(host_time);
742 
743 				host_time = get_host_poll_time();
744 				host_time_double = get_time_as_double();
745 			}
746 		} else {
747 			itemsPT = 0;
748 
749 			if (host_time) free(host_time);
750 
751 			host_time = get_host_poll_time();
752 			host_time_double = get_time_as_double();
753 		}
754 
755 		/* populate the thread structure */
756 		if (!(poller_details = (poller_thread_t *)malloc(sizeof(poller_thread_t)))) {
757 			die("ERROR: Fatal malloc error: spine.c poller_details!");
758 		}
759 
760 		poller_details->host_id          = host_id;
761 		poller_details->host_thread      = current_thread;
762 		poller_details->last_host_thread = device_threads;
763 		poller_details->host_data_ids    = itemsPT;
764 		poller_details->host_time        = host_time;
765 		poller_details->host_time_double = host_time_double;
766 		poller_details->thread_init_sem  = &thread_init_sem;
767 		poller_details->complete         = FALSE;
768 
769 		details[device_counter]          = poller_details;
770 
771 		retry1:
772 
773 		if (sem_timedwait(&active_threads, &until_spec) == -1) {
774 			if (errno == ETIMEDOUT) {
775 				SPINE_LOG(("ERROR: Spine Timed Out While Processing Devices Internal"));
776 				canexit = TRUE;
777 				break;
778 			} else if (errno == EINTR) {
779 				usleep(10000);
780 				goto retry1;
781 			}
782 		}
783 
784 		/* create child process */
785 		sem_wait(&thread_init_sem);
786 
787 		thread_status = pthread_create(&threads[device_counter], &attr, child, poller_details);
788 
789 		switch (thread_status) {
790 			case 0:
791 				SPINE_LOG_DEBUG(("DEBUG: Valid Thread to be Created"));
792 
793 				if (change_host) {
794 					device_counter++;
795 				}
796 
797 				sem_wait(&thread_init_sem);
798 				thread_mutex_lock(LOCK_PEND);
799 				pending_threads++;
800 
801 				sem_getvalue(&active_threads, &a_threads_value);
802 				SPINE_LOG_MEDIUM(("Active Threads is %i, Pending is %i", set.threads - a_threads_value, pending_threads));
803 
804 				thread_mutex_unlock(LOCK_PEND);
805 				sem_post(&thread_init_sem);
806 
807 				SPINE_LOG_DEVDBG(("DEBUG: DTS: device = %d, host_id = %d, host_thread = %d,"
808 					" last_host_thread = %d, host_data_ids = %d, complete = %d",
809 					device_counter-1,
810 					poller_details->host_id,
811 					poller_details->host_thread,
812 					poller_details->last_host_thread,
813 					poller_details->host_data_ids,
814 					poller_details->complete));
815 
816 				break;
817 			case EAGAIN:
818 				SPINE_LOG(("ERROR: The System Lacked the Resources to Create a Thread"));
819 				break;
820 			case EFAULT:
821 				SPINE_LOG(("ERROR: The Thread or Attribute were Invalid"));
822 				break;
823 			case EINVAL: SPINE_LOG(("ERROR: The Thread Attribute is Not Initialized"));
824 				break;
825 			default:
826 				SPINE_LOG(("ERROR: Unknown Thread Creation Error"));
827 				break;
828 		}
829 
830 		/* Restore thread initialization semaphore if thread creation failed */
831 		if (thread_status) {
832 			sem_post(&thread_init_sem);
833 		}
834 	}
835 
836 	/* wait for all threads to 'complete'
837  	 * using the mutex here as the semaphore will
838      * show zero before the children are done */
839 	while (TRUE) {
840 		thread_mutex_lock(LOCK_PEND);
841 
842 		cur_time = get_time_as_double();
843 
844 		if (pending_threads == 0) {
845 			break;
846 		} else if (cur_time - begin_time > set.poller_interval) {
847 			SPINE_LOG(("ERROR: Spine Timed Out While Waiting for %d Threads to End", pending_threads));
848 			break;
849 		}
850 
851 		thread_mutex_unlock(LOCK_PEND);
852 
853 		usleep(100000);
854 	}
855 
856 	sem_getvalue(&active_threads, &a_threads_value);
857 
858 	threads_final = set.threads - a_threads_value;
859 
860 	if (threads_final) {
861 		SPINE_LOG_HIGH(("The final count of Threads is %i", threads_final));
862 
863 		for (threads_count = 0; threads_count < num_rows; threads_count++) {
864 			poller_thread_t* det = details[threads_count];
865 
866 			if (threads_missing == -1 && det == NULL) {
867 				threads_missing = threads_count;
868 			}
869 
870 			if (det != NULL) { // && !det->complete) {
871 				SPINE_LOG_HIGH(("INFO: Thread %scomplete for Device[%d] and %d to %d sources",
872 					det->complete?"":"in",
873 					det->host_id,
874 					det->host_data_ids * (det->host_thread - 1),
875 					det->host_data_ids * (det->host_thread)));
876 
877 				SPINE_LOG_DEVDBG(("DEBUG: DTF: device = %d, host_id = %d, host_thread = %d,"
878 					" last_host_thread = %d, host_data_ids = %d, complete = %d",
879 					threads_count,
880 					det->host_id,
881 					det->host_thread,
882 					det->last_host_thread,
883 					det->host_data_ids,
884 					det->complete));
885 			}
886 		}
887 
888 		if (threads_missing > -1) {
889 			SPINE_LOG_HIGH(("ERROR: There were %d threads which did not run", num_rows - threads_missing));
890 		}
891 	} else {
892 		SPINE_LOG_MEDIUM(("The Final Value of Threads is %i", threads_final));
893 	}
894 
895 	/* tell Spine that it is now parent */
896 	set.parent_fork = SPINE_PARENT;
897 
898 	/* push data back to the main server */
899 	if (set.poller_id > 1 && set.mode == REMOTE_ONLINE && !set.SQL_readonly) {
900 		poller_push_data_to_main();
901 	}
902 
903 	/* print out stats */
904 	gettimeofday(&now, NULL);
905 
906 	/* update the db for |data_time| on graphs */
907 	if (set.poller_id == 1) {
908 		db_insert(&mysql, LOCAL, "REPLACE INTO settings (name,value) VALUES ('date',NOW())");
909 	}
910 
911 	snprintf(querybuf, BIG_BUFSIZE, "UPDATE poller_time SET end_time=NOW() WHERE poller_id=%i AND pid=%i", set.poller_id, getpid());
912 	if (mode == REMOTE) {
913 		db_insert(&mysqlr, REMOTE, querybuf);
914 	} else {
915 		db_insert(&mysql, LOCAL, querybuf);
916 	}
917 
918 	if (db_pool_local) {
919 		db_close_connection_pool(LOCAL);
920 	}
921 
922 	if (db_pool_remote) {
923 		db_close_connection_pool(REMOTE);
924 	}
925 
926 	/* cleanup and exit program */
927 	pthread_attr_destroy(&attr);
928 
929 	SPINE_LOG_DEBUG(("DEBUG: Thread Cleanup Complete"));
930 
931 	/* close the php script server */
932 	if (set.php_required) {
933 		php_close(PHP_INIT);
934 	}
935 
936 	SPINE_LOG_DEBUG(("DEBUG: PHP Script Server Pipes Closed"));
937 
938 	/* free malloc'd variables */
939 	for (i = 0; i < num_rows; i++) {
940 		if (details[i] != NULL) {
941 			free(details[i]);
942 		}
943 	}
944 
945 	free(details);
946 	free(threads);
947 	free(ids);
948 	free(conf_file);
949 	free(debug_devices);
950 
951 	SPINE_LOG_DEBUG(("DEBUG: Allocated Variable Memory Freed"));
952 
953 	/* close mysql */
954 	db_free_result(result);
955 	db_disconnect(&mysql);
956 
957 	if (set.poller_id > 1 && set.mode == REMOTE_ONLINE) {
958 		db_disconnect(&mysqlr);
959 	}
960 
961 	SPINE_LOG_DEBUG(("DEBUG: MYSQL Free & Close Completed"));
962 
963 	/* close snmp */
964 	snmp_spine_close();
965 
966 	SPINE_LOG_DEBUG(("DEBUG: Net-SNMP Close Completed"));
967 
968 	/* finally add some statistics to the log and exit */
969 	end_time = TIMEVAL_TO_DOUBLE(now);
970 
971 	if (set.log_level >= POLLER_VERBOSITY_MEDIUM) {
972 		SPINE_LOG(("Time: %.4f s, Threads: %i, Devices: %i", (end_time - begin_time), set.threads, num_rows));
973 	} else {
974 		/* provide output if running from command line */
975 		if (!set.stdout_notty) {
976 			fprintf(stdout,"Time: %.4f s, Threads: %i, Devices: %i\n", (end_time - begin_time), set.threads, num_rows);
977 		}
978 	}
979 
980 	/* uninstall the spine signal handler */
981 	uninstall_spine_signal_handler();
982 
983 	/* clueanup winsock library on Windows */
984 	SOCK_CLEANUP;
985 
986 	exit(EXIT_SUCCESS);
987 }
988 
989 /*! \fn static void display_help()
990  *  \brief Display Spine usage information to the caller.
991  *
992  *	Display the help listing: the first line is created at runtime with
993  *	the version information, and the rest is strictly static text which
994  *	is dumped literally.
995  *
996  */
display_help(int only_version)997 static void display_help(int only_version) {
998 	static const char *const *p;
999 	static const char * const helptext[] = {
1000 		"Usage: spine [options] [[firstid lastid] || [-H/--hostlist='hostid1,hostid2,...,hostidn']]",
1001 		"",
1002 		"Options:",
1003 		"  -h/--help          Show this brief help listing",
1004 		"  -f/--first=X       Start polling with host id X",
1005 		"  -l/--last=X        End polling with host id X",
1006 		"  -H/--hostlist=X    Poll the list of host ids, separated by comma's",
1007 		"  -p/--poller=X      Set the poller id to X",
1008 		"  -C/--conf=F        Read spine configuration from file F",
1009 		"  -O/--option=S:V    Override DB settings 'set' with value 'V'",
1010 		"  -M/--mibs          Refresh the device System Mib data",
1011 		"  -N/--mode=online   For remote pollers, the operating mode.",
1012 		"                     Options include: online, offline, recovery.",
1013 		"                     The default is 'online'.",
1014 		"  -R/--readonly      Spine will not write output to the DB",
1015 		"  -S/--stdout        Logging is performed to standard output",
1016 		"  -V/--verbosity=V   Set logging verbosity to <V>",
1017 		"",
1018 		"Either both of --first/--last must be provided, a valid hostlist must be provided.",
1019         "In their absence, all hosts are processed.",
1020 		"",
1021 		"Without the --conf parameter, spine searches for its spine.conf",
1022 		"file in the usual places.",
1023 		"",
1024 		"Verbosity is one of NONE/LOW/MEDIUM/HIGH/DEBUG or 1..5",
1025 		"",
1026 		"Runtime options are read from the 'settings' table in the Cacti",
1027 		"database, but they can be overridden with the --option=S:V",
1028 		"parameter.",
1029 		"",
1030 		"Spine is distributed under the Terms of the GNU Lesser",
1031 		"General Public License Version 2.1. (http://www.gnu.org/licenses/lgpl.txt)",
1032 		"For more information, see http://www.cacti.net",
1033 
1034 		0 /* ENDMARKER */
1035 	};
1036 
1037 	printf("SPINE %s  Copyright 2004-2021 by The Cacti Group\n", VERSION);
1038 
1039 	if (only_version == FALSE) {
1040 		printf("\n");
1041 		for (p = helptext; *p; p++) {
1042 			puts(*p);	/* automatically adds a newline */
1043 		}
1044 	}
1045 }
1046 
1047 /*! \fn static char *getarg(char *opt, char ***pargv)
1048  *  \brief A function to parse calling parameters
1049  *
1050  *	This is a helper for the main arg-processing loop: we work with
1051  *	options which are either of the form "-X=FOO" or "-X FOO"; we
1052  *	want an easy way to handle either one.
1053  *
1054  *	The idea is that if the parameter has an = sign, we use the rest
1055  *	of that same argv[X] string, otherwise we have to get the *next*
1056  *	argv[X] string. But it's an error if an option-requiring param
1057  *	is at the end of the list with no argument to follow.
1058  *
1059  *	The option name could be of the form "-C" or "--conf", but we
1060  *	grab it from the existing argv[] so we can report it well.
1061  *
1062  * \return character pointer to the argument
1063  *
1064  */
getarg(char * opt,char *** pargv)1065 static char *getarg(char *opt, char ***pargv) {
1066 	const char *const optname = **pargv;
1067 
1068 	/* option already set? */
1069 	if (opt) return opt;
1070 
1071 	/* advance to next argv[] and try that one */
1072 	if ((opt = *++(*pargv)) != 0) return opt;
1073 
1074 	die("ERROR: option %s requires a parameter", optname);
1075 }
1076