1<?php
2
3/**
4 * Observium
5 *
6 *   This file is part of Observium.
7 *
8 * @package    observium
9 * @subpackage rrdtool
10 * @copyright  (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited
11 *
12 */
13
14/**
15 * Get full path for rrd file.
16 *
17 * @param array $device Device array
18 * @param string $filename Base filename for rrd file
19 * @return string Full rrd file path
20 */
21// TESTME needs unit testing
22function get_rrd_path($device, $filename)
23{
24  global $config;
25
26  $rrd_dir = trim($config['rrd_dir']) . '/';
27  $filename = trim($filename);
28  if (str_starts($filename, $rrd_dir))
29  {
30    // Already full path
31    return $filename;
32  }
33
34  $filename = safename($filename);
35
36  // If filename empty, return base rrd dirname for device (for example in delete_device())
37  $rrd_file = $rrd_dir;
38  if (strlen($device['hostname']))
39  {
40    $rrd_file .= $device['hostname'] . '/';
41  }
42
43  if (strlen($filename) > 0)
44  {
45    $ext = pathinfo($filename, PATHINFO_EXTENSION);
46    if ($ext != 'rrd') { $filename .= '.rrd'; } // Add rrd extension if not already set
47    $rrd_file .= safename($filename);
48
49    // Add rrd filename to global array $graph_return
50    $GLOBALS['graph_return']['rrds'][] = $rrd_file;
51  }
52
53  return $rrd_file;
54}
55
56/**
57 * Rename rrd file for device is some schema changes.
58 *
59 * @param array $device
60 * @param string $old_rrd Base filename for old rrd file
61 * @param string $new_rrd Base filename for new rrd file
62 * @param boolean $overwrite Force overwrite new rrd file if already exist
63 * @return bool TRUE if renamed
64 */
65function rename_rrd($device, $old_rrd, $new_rrd, $overwrite = FALSE)
66{
67  $old_rrd = get_rrd_path($device, $old_rrd);
68  $new_rrd = get_rrd_path($device, $new_rrd);
69  print_debug_vars($old_rrd);
70  print_debug_vars($new_rrd);
71  if (is_file($old_rrd))
72  {
73    if (!$overwrite && is_file($new_rrd))
74    {
75      // If not forced overwrite file, return false
76      print_debug("RRD already exist new file: '$new_rrd'");
77      $renamed = FALSE;
78    } else {
79      $renamed = rename($old_rrd, $new_rrd);
80    }
81  } else {
82    print_debug("RRD old file not found: '$old_rrd'");
83    $renamed = FALSE;
84  }
85  if ($renamed)
86  {
87    print_debug("RRD moved: '$old_rrd' -> '$new_rrd'");
88  }
89
90  return $renamed;
91}
92
93/**
94 * Rename rrd file for device (same as in rename_rrd()),
95 * but rrd filename detected by common entity params
96 *
97 * @param array $device
98 * @param string $entity Entity type (sensor, status, etc..)
99 * @param array $old Old entity params, based on discovery entity
100 * @param array $new New entity params, based on discovery entity
101 * @param boolean $overwrite Force overwrite new rrd file if already exist
102 * @return bool TRUE if renamed
103 */
104function rename_rrd_entity($device, $entity, $old, $new, $overwrite = FALSE)
105{
106  switch ($entity)
107  {
108    case 'sensor':
109      $old_sensor = array('poller_type'  => $old['poller_type'],
110                          'sensor_descr' => $old['descr'],
111                          'sensor_class' => $old['class'],
112                          'sensor_type'  => $old['type'],
113                          'sensor_index' => $old['index']);
114      $new_sensor = array('poller_type'  => $new['poller_type'],
115                          'sensor_descr' => $new['descr'],
116                          'sensor_class' => $new['class'],
117                          'sensor_type'  => $new['type'],
118                          'sensor_index' => $new['index']);
119
120      $old_rrd = get_sensor_rrd($device, $old_sensor);
121      $new_rrd = get_sensor_rrd($device, $new_sensor);
122      break;
123    case 'status':
124      $old_status = array('poller_type'  => $old['poller_type'],
125                          'status_descr' => $old['descr'],
126                          'status_type'  => $old['type'],
127                          'status_index' => $old['index']);
128      $new_status = array('poller_type'  => $new['poller_type'],
129                          'status_descr' => $new['descr'],
130                          'status_type'  => $new['type'],
131                          'status_index' => $new['index']);
132
133      $old_rrd = get_status_rrd($device, $old_status);
134      $new_rrd = get_status_rrd($device, $new_status);
135      break;
136    default:
137      print_debug("skipped unknown entity for rename rrd");
138      return FALSE;
139  }
140
141  $old_rrd = safename($old_rrd);
142
143  return rename_rrd($device, $old_rrd, $new_rrd, $overwrite);
144}
145
146/**
147 * Opens up a pipe to RRDTool using handles provided
148 *
149 * @return boolean
150 * @global array $config
151 * @param &rrd_process
152 * @param &rrd_pipes
153 */
154// TESTME needs unit testing
155function rrdtool_pipe_open(&$rrd_process, &$rrd_pipes)
156{
157  global $config;
158
159  $command = $config['rrdtool'] . ' -'; // Waits for input via standard input (STDIN)
160
161  $descriptorspec = array(
162     0 => array('pipe', 'r'),  // stdin
163     1 => array('pipe', 'w'),  // stdout
164     2 => array('pipe', 'w')   // stderr
165  );
166
167  $cwd = $config['rrd_dir'];
168  $env = array();
169
170  $rrd_process = proc_open($command, $descriptorspec, $rrd_pipes, $cwd, $env);
171
172  stream_set_blocking($rrd_pipes[1], 0);
173  stream_set_blocking($rrd_pipes[2], 0);
174
175  if (is_resource($rrd_process))
176  {
177    // $pipes now looks like this:
178    // 0 => writeable handle connected to child stdin
179    // 1 => readable handle connected to child stdout
180    // 2 => readable handle connected to child stderr
181    if (OBS_DEBUG > 1)
182    {
183      print_message('RRD PIPE OPEN[%gTRUE%n]', 'console');
184    }
185
186    return TRUE;
187  } else {
188    if (isset($config['rrd']['debug']) && $config['rrd']['debug'])
189    {
190      logfile('rrd.log', "RRD pipe process not opened '$command'.");
191    }
192    if (OBS_DEBUG > 1)
193    {
194      print_message('RRD PIPE OPEN[%rFALSE%n]', 'console');
195    }
196    return FALSE;
197  }
198}
199
200/**
201 * Closes the pipe to RRDTool
202 *
203 * @return integer
204 * @param resource rrd_process
205 * @param array rrd_pipes
206 */
207// TESTME needs unit testing
208function rrdtool_pipe_close($rrd_process, &$rrd_pipes)
209{
210  if (OBS_DEBUG > 1)
211  {
212    $rrd_status['stdout'] = stream_get_contents($rrd_pipes[1]);
213    $rrd_status['stderr'] = stream_get_contents($rrd_pipes[2]);
214  }
215
216  if (is_resource($rrd_pipes[0]))
217  {
218    fclose($rrd_pipes[0]);
219  }
220  fclose($rrd_pipes[1]);
221  fclose($rrd_pipes[2]);
222
223  // It is important that you close any pipes before calling
224  // proc_close in order to avoid a deadlock
225
226  $rrd_status['exitcode'] = proc_close($rrd_process);
227  if (OBS_DEBUG > 1)
228  {
229    print_message('RRD PIPE CLOSE['.($rrd_status['exitcode'] !== 0 ? '%rFALSE' : '%gTRUE').'%n]', 'console');
230    if ($rrd_status['stdout'])
231    {
232      print_message("RRD PIPE STDOUT[\n".$rrd_status['stdout']."\n]", 'console', FALSE);
233    }
234    if ($rrd_status['exitcode'] && $rrd_status['stderr'])
235    {
236      // Show stderr if exitcode not 0
237      print_message("RRD PIPE STDERR[\n".$rrd_status['stderr']."\n]", 'console', FALSE);
238    }
239  }
240
241  return $rrd_status['exitcode'];
242}
243
244/**
245 * Generates a graph file at $graph_file using $options
246 * Opens its own rrdtool pipe.
247 *
248 * @return integer
249 * @param string graph_file
250 * @param string options
251 */
252// TESTME needs unit testing
253function rrdtool_graph($graph_file, $options)
254{
255  global $config;
256
257  // Note, always use pipes, because standard command line has limits!
258  if ($config['rrdcached'])
259  {
260    $options = str_replace($config['rrd_dir'].'/', '', $options);
261    $cmd = 'graph --daemon ' . $config['rrdcached'] . " $graph_file $options";
262  } else {
263    $cmd = "graph $graph_file $options";
264  }
265  $GLOBALS['rrd_status']  = FALSE;
266  $GLOBALS['exec_status'] = array('command'  => $config['rrdtool'] . ' ' . $cmd,
267                                  'stdout'   => '',
268                                  'exitcode' => -1);
269
270  $start = microtime(TRUE);
271  rrdtool_pipe_open($rrd_process, $rrd_pipes);
272  if (is_resource($rrd_process))
273  {
274    // $pipes now looks like this:
275    // 0 => writeable handle connected to child stdin
276    // 1 => readable handle connected to child stdout
277    // Any error output will be appended to /tmp/error-output.txt
278
279    fwrite($rrd_pipes[0], $cmd);
280    fclose($rrd_pipes[0]);
281
282    $iter = 0;
283    while (strlen($line) < 1 && $iter < 1000)
284    {
285      // wait for 10 milliseconds to loosen loop
286      usleep(10000);
287      $line = fgets($rrd_pipes[1], 1024);
288      $stdout .= $line;
289      $iter++;
290    }
291    $stdout = preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
292    unset($iter);
293
294    $runtime  = microtime(TRUE) - $start;
295
296    // Check rrdtool's output for the command.
297    if (preg_match('/\d+x\d+/', $stdout))
298    {
299      $GLOBALS['rrd_status'] = TRUE;
300    } else {
301      $stderr = trim(stream_get_contents($rrd_pipes[2]));
302      if (isset($config['rrd']['debug']) && $config['rrd']['debug'])
303      {
304        logfile('rrd.log', "RRD $stderr, CMD: " . $GLOBALS['exec_status']['command']);
305      }
306    }
307    $exitcode = rrdtool_pipe_close($rrd_process, $rrd_pipes);
308
309    $GLOBALS['exec_status']['exitcode'] = $exitcode;
310    $GLOBALS['exec_status']['stdout']   = $stdout;
311    $GLOBALS['exec_status']['stderr']   = $stderr;
312  } else {
313    $runtime = microtime(TRUE) - $start;
314    $stdout  = NULL;
315  }
316  $GLOBALS['exec_status']['runtime']  = $runtime;
317  // Add some data to global array $graph_return
318  $GLOBALS['graph_return']['status']   = $GLOBALS['rrd_status'];
319  $GLOBALS['graph_return']['command']  = $GLOBALS['exec_status']['command'];
320  $GLOBALS['graph_return']['filename'] = $graph_file;
321  $GLOBALS['graph_return']['output']   = $stdout;
322  $GLOBALS['graph_return']['runtime']  = $GLOBALS['exec_status']['runtime'];
323
324  if (OBS_DEBUG)
325  {
326    print_message(PHP_EOL . 'RRD CMD[%y' . $cmd . '%n]', 'console', FALSE);
327    $debug_msg  = 'RRD RUNTIME['.($runtime > 0.1 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL;
328    $debug_msg .= 'RRD STDOUT['.($GLOBALS['rrd_status'] ? '%g': '%r').$stdout.'%n]' . PHP_EOL;
329    if ($stderr)
330    {
331      $debug_msg .= 'RRD STDERR[%r'.$stderr.'%n]' . PHP_EOL;
332    }
333    $debug_msg .= 'RRD_STATUS['.($GLOBALS['rrd_status'] ? '%gTRUE': '%rFALSE').'%n]';
334
335    print_message($debug_msg . PHP_EOL, 'console');
336  }
337
338  return $stdout;
339}
340
341/**
342 * Generates and pipes a command to rrdtool
343 *
344 * @param string command
345 * @param string filename
346 * @param string options
347 * @global array $config
348 * @global mixed $rrd_pipes
349 */
350// TESTME needs unit testing
351function rrdtool($command, $filename, $options)
352{
353  global $config, $rrd_pipes;
354
355  // We now require rrdcached 1.5.5
356  if ($config['rrdcached'] && (OBS_RRD_NOLOCAL || !in_array($command, ['create', 'tune'])))
357  {
358    $filename = str_replace($config['rrd_dir'].'/', '', $filename);
359    if (OBS_RRD_NOLOCAL && $command == 'create')
360    {
361      // No overwrite for remote rrdtool, since no way for check if rrdfile exist
362      $options  .= ' --no-overwrite';
363    }
364    $options  .= ' --daemon ' . $config['rrdcached'];
365  }
366
367  $cmd = "$command $filename $options";
368
369  $GLOBALS['rrd_status'] = FALSE;
370  $GLOBALS['exec_status'] = array('command' => $config['rrdtool'] . ' ' . $cmd,
371                                  'exitcode' => 1);
372
373  if ($config['norrd'])
374  {
375    print_message("[%rRRD Disabled - $cmd%n]", 'color');
376    return NULL;
377  }
378
379  if (in_array($command, array('fetch', 'last', 'lastupdate', 'tune')))
380  {
381    // This commands require exact STDOUT, skip use pipes
382    $command = $config['rrdtool'] . ' ' . $cmd;
383    $stdout = external_exec($command, 500); // Limit exec time to 500ms
384    $runtime = $GLOBALS['exec_status']['runtime'];
385    $GLOBALS['rrd_status'] = $GLOBALS['exec_status']['exitcode'] === 0;
386    // Check rrdtool's output for the command.
387    if (!$GLOBALS['rrd_status'] && isset($config['rrd']['debug']) && $config['rrd']['debug'])
388    {
389      logfile('rrd.log', "RRD ".$GLOBALS['exec_status']['stderr'].", CMD: $cmd");
390    }
391  } else {
392    // FIXME, need add check if pipes exist
393    $start = microtime(TRUE);
394    fwrite($rrd_pipes[0], $cmd."\n");
395    usleep(1000);
396
397    $stdout = trim(stream_get_contents($rrd_pipes[1]));
398    $stderr = trim(stream_get_contents($rrd_pipes[2]));
399    $runtime = microtime(TRUE) - $start;
400
401    // Check rrdtool's output for the command.
402    if (strpos($stdout, 'ERROR') !== FALSE)
403    {
404      if (isset($config['rrd']['debug']) && $config['rrd']['debug'])
405      {
406        logfile('rrd.log', "RRD $stdout, CMD: $cmd");
407      }
408    } else {
409      $GLOBALS['rrd_status'] = TRUE;
410      $GLOBALS['exec_status']['exitcode'] = 0;
411    }
412    $GLOBALS['exec_status']['stdout']  = $stdout;
413    $GLOBALS['exec_status']['stdin']   = $stdin;
414    $GLOBALS['exec_status']['runtime'] = $runtime;
415
416  }
417
418  $GLOBALS['rrdtool'][$command]['time'] += $runtime;
419  $GLOBALS['rrdtool'][$command]['count']++;
420
421  if (OBS_DEBUG)
422  {
423    print_message(PHP_EOL . 'RRD CMD[%y' . $cmd . '%n]', 'console', FALSE);
424    $debug_msg  = 'RRD RUNTIME['.($runtime > 1 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL;
425    $debug_msg .= 'RRD STDOUT['.($GLOBALS['rrd_status'] ? '%g': '%r').$stdout.'%n]' . PHP_EOL;
426    if ($stderr)
427    {
428      $debug_msg .= 'RRD STDERR[%r'.$stderr.'%n]' . PHP_EOL;
429    }
430    $debug_msg .= 'RRD_STATUS['.($GLOBALS['rrd_status'] ? '%gTRUE': '%rFALSE').'%n]';
431
432    print_message($debug_msg . PHP_EOL, 'console');
433  }
434
435  return $stdout;
436}
437
438/**
439 * Generates an rrd database at $filename using $options
440 * Creates the file if it does not exist yet.
441 * DEPRECATED: use rrdtool_create_ng(), this will disappear and ng will be renamed when conversion is complete.
442 *
443 * @param array  device
444 * @param string filename
445 * @param string ds
446 * @param string options
447 */
448function rrdtool_create($device, $filename, $ds, $options = '')
449{
450  global $config;
451
452  if ($filename[0] == '/')
453  {
454    print_debug("You should pass the filename only (not the full path) to this function! Passed filename: ".$filename);
455    $filename = basename($filename);
456  }
457
458  $fsfilename = get_rrd_path($device, $filename);
459
460  if ($config['norrd'])
461  {
462    print_message("[%rRRD Disabled - create $fsfilename%n]", 'color');
463    return NULL;
464  }
465  else if (OBS_RRD_NOLOCAL)
466  {
467    print_debug("RRD create $fsfilename passed to remote rrdcached with --no-overwrite.");
468  }
469  else if (rrd_exists($device, $filename))
470  {
471    print_debug("RRD $fsfilename already exists - no need to create.");
472    return FALSE; // Bail out if the file exists already
473  }
474
475  if (!$options)
476  {
477    $options = preg_replace('/\s+/', ' ', $config['rrd']['rra']);
478  }
479
480  $step = "--step ".$config['rrd']['step'];
481
482  //$command = $config['rrdtool'] . " create $fsfilename $ds $step $options";
483  //return external_exec($command);
484
485  // Clean up old ds strings. This is kinda nasty.
486  $ds = str_replace("\
487", '', $ds);
488  return rrdtool('create', $fsfilename, $ds . " $step $options");
489
490}
491
492/**
493 * Generates RRD filename from definition
494 *
495 * @param string/array $def    Original filename, using %index% (or %custom% %keys%) as placeholder for indexes
496 * @param string/array $index  Index, if RRD type is indexed (or array of multiple indexes)
497 * @return string              Filename of RRD
498 */
499// TESTME needs unit testing
500function rrdtool_generate_filename($def, $index)
501{
502  if (is_string($def))
503  {
504    // Compat with old
505    $filename = $def;
506  }
507  elseif (isset($def['file']))
508  {
509    $filename = $def['file'];
510  }
511  elseif (isset($def['entity_type']))
512  {
513    // Entity specific filename by ID, ie for sensor/status/counter
514    $entity_id = $index;
515    return get_entity_rrd_by_id($def['entity_type'], $entity_id);
516  }
517
518  // Generate warning for indexed filenames containing %index% - does not help if you use custom field names for indexing
519  if (strstr($filename, '%index%') !== FALSE)
520  {
521    if ($index === NULL)
522    {
523      print_warning("RRD filename generation error: filename contains %index%, but \$index is NULL!");
524    }
525  }
526
527  // Convert to single element array if not an array.
528  // This will automatically use %index% as the field to replace (see below).
529  if (!is_array($index)) { $index = array('index' => $index); }
530
531  // Replace %index% by $index['index'], %foo% by $index['foo'] etc.
532  $filename = array_tag_replace($index, $filename);
533
534  return safename($filename);
535}
536
537/**
538 * Generates an rrd database based on $type definition, using $options
539 * Only creates the file if it does not exist yet.
540 * Should most likely not be called on its own, as an update call will check for existence.
541 *
542 * @param array        $device   Device array
543 * @param string/array $type     rrd file type from $config['rrd_types'] or actual config array
544 * @param string/array $index    Index, if RRD type is indexed (or array of multiple tags)
545 * @param array        $options  Options for create RRD, like STEP, RRA, MAX or SPEED
546 *
547 * @return string
548 */
549// TESTME needs unit testing
550function rrdtool_create_ng($device, $type, $index = NULL, $options = [])
551{
552  global $config;
553
554  if (!is_array($type)) // We were passed a string
555  {
556    if (!is_array($config['rrd_types'][$type])) // Check if definition exists
557    {
558      print_warning("Cannot create RRD for type $type - not found in definitions!");
559      return FALSE;
560    }
561
562    $definition = $config['rrd_types'][$type];
563  } else { // We were passed an array, use as-is
564    $definition = $type;
565  }
566
567  $filename = rrdtool_generate_filename($definition, $index);
568
569  $fsfilename = get_rrd_path($device, $filename);
570
571  if ($config['norrd'])
572  {
573    print_message("[%rRRD Disabled - create $fsfilename%n]", 'color');
574    return NULL;
575  }
576  else if (OBS_RRD_NOLOCAL)
577  {
578    print_debug("RRD create $fsfilename passed to remote rrdcached with --no-overwrite.");
579  }
580  else if (rrd_exists($device, $filename))
581  {
582    print_debug("RRD $fsfilename already exists - no need to create.");
583    return FALSE; // Bail out if the file exists already
584  }
585
586  // Set RRA option
587  $rra = isset($options['rra']) ? $options['rra'] : $config['rrd']['rra'];
588  $rra = preg_replace('/\s+/', ' ', $rra);
589
590  // Set step
591  $step = isset($options['step']) ? $options['step'] : $config['rrd']['step'];
592
593  // Create tags, for use in replace
594  $tags = [];
595  if (strlen($index))
596  {
597    $tags['index'] = $index;
598  }
599  if (isset($options['speed']))
600  {
601    print_debug("Passed speed: ".$options['speed']);
602    $options['speed'] = intval(unit_string_to_numeric($options['speed']) / 8); // Detect passed speed value (converted to bits)
603    $tags['speed']    = max($options['speed'], $config['max_port_speed']);     // But result select maximum between passed and default!
604    print_debug("   RRD speed: ".$options['speed'].PHP_EOL.
605                "     Default: ".$config['max_port_speed'].PHP_EOL.
606                "         Max: ".$tags['speed']);
607  } else {
608    // Default speed
609    $tags['speed'] = $config['max_port_speed'];
610  }
611
612  // Create DS parameter based on the definition
613  $ds = array();
614
615  foreach ($definition['ds'] as $name => $def)
616  {
617    if (strlen($name) > 19) { print_warning("SEVERE: DS name $name is longer than 19 characters - over RRD limit!"); }
618
619    // Set defaults for missing attributes
620    if (!isset($def['type']))      { $def['type'] = 'COUNTER'; }
621    if (!isset($def['max']))       { $def['max'] = 'U'; }
622    else                           { $def['max'] = array_tag_replace($tags, $def['max']); } // can use %speed% tag, speed must passed by $options['speed']
623    if (!isset($def['min']))       { $def['min'] = 'U'; }
624    if (!isset($def['heartbeat'])) { $def['heartbeat'] = 2 * $step; }
625
626    // Create DS string to pass on the command line
627    $ds[] = "DS:$name:" . $def['type'] . ':' . $def['heartbeat'] . ':' . $def['min'] . ':' . $def['max'];
628  }
629
630
631  return rrdtool('create', $fsfilename, implode(' ', $ds) . " --step $step $rra");
632
633}
634
635/**
636 * Checks if an RRD database at $filename for $device exists
637 * Checks via rrdcached if configured, else via is_exists
638 *
639 * @param array  device
640 * @param string filename
641**/
642function rrd_exists($device, $filename)
643{
644
645  global $config;
646
647  $fsfilename = get_rrd_path($device, $filename);
648
649  if (OBS_RRD_NOLOCAL)
650  {
651    // NOTE. RRD last on remote daemon reduce polling times
652    rrdtool_last($fsfilename);
653
654    //ERROR: realpath(vds.coosm.net/status.rrd): No such file or directory
655    return strpos($GLOBALS['exec_status']['stderr'], 'No such file or directory') === FALSE;
656    //return $GLOBALS['rrd_status'];
657  } else {
658    if (is_file($fsfilename))
659    {
660      return TRUE;
661    } else {
662      return FALSE;
663    }
664  }
665
666}
667
668/**
669 * Updates an rrd database at $filename using $options
670 * Where $options is an array, each entry which is not a number is replaced with "U"
671 *
672 * @param array        $device  Device array
673 * @param string/array $type    RRD file type from $config['rrd_types'] or actual config array
674 * @param array        $ds      DS data (key/value)
675 * @param string/array $index   Index, if RRD type is indexed (or array of multiple indexes)
676 * @param bool         $create  Create RRD file if it does not exist
677 * @param array        $options Options to pass to create function if file does not exist
678 *
679 * @return string
680 */
681// TESTME needs unit testing
682function rrdtool_update_ng($device, $type, $ds, $index = NULL, $create = TRUE, $options = [])
683{
684  global $config, $graphs;
685
686  if (!is_array($type)) // We were passed a string
687  {
688    if (!is_array($config['rrd_types'][$type])) // Check if definition exists
689    {
690      print_warning("Cannot create RRD for type $type - not found in definitions!");
691      return FALSE;
692    }
693
694    $definition = $config['rrd_types'][$type];
695
696    // Append graph if not already passed
697    if (!isset($definition['graphs']))
698    {
699      $definition['graphs'] = array(str_replace('-', '_', $type));
700    }
701  } else { // We were passed an array, use as-is
702    $definition = $type;
703  }
704
705  $filename = rrdtool_generate_filename($definition, $index);
706
707  $fsfilename = get_rrd_path($device, $filename);
708
709  // Create the file if missing (if we have permission to create it)
710  if ($create)
711  {
712    rrdtool_create_ng($device, $type, $index, $options);
713  }
714
715  $update = array('N');
716
717  foreach ($definition['ds'] as $name => $def)
718  {
719    if (isset($ds[$name]))
720    {
721      if (is_numeric($ds[$name]))
722      {
723        // Add data to DS update string
724        $update[] = $ds[$name];
725      } else {
726        // Data not numeric, mark unknown
727        $update[] = 'U';
728      }
729    } else {
730      // Data not sent, mark unknown
731      $update[] = 'U';
732    }
733  }
734
735  /** // This is setting loads of random shit that doesn't exist
736      // ONLY GRAPHS THAT EXIST MAY GO INTO THIS ARRAY
737  // Set global graph variable for store avialable device graphs
738  foreach ($definition['graphs'] as $def)
739  {
740    $graphs[$def] = TRUE;
741  }
742  **/
743
744  if ($config['influxdb']['enabled'])
745  {
746    influxdb_update($device, $filename, $ds, $definition, $index);
747  }
748
749  return rrdtool('update', $fsfilename, implode(':', $update));
750}
751
752/**
753 * Updates an rrd database at $filename using $options
754 * Where $options is an array, each entry which is not a number is replaced with "U"
755 * DEPRECATED: use rrdtool_update_ng(), this will disappear and ng will be renamed when conversion is complete.
756 *
757 * @param array   $device
758 * @param string  $filename
759 * @param array   $options
760 * @return string
761 */
762function rrdtool_update($device, $filename, $options)
763{
764  // Do some sanitization on the data if passed as an array.
765  if (is_array($options))
766  {
767    $values[] = "N";
768    foreach ($options as $value)
769    {
770      if (!is_numeric($value)) { $value = 'U'; }
771      $values[] = $value;
772    }
773    $options = implode(':', $values);
774  }
775
776  if ($filename[0] == '/')
777  {
778    $filename = basename($filename);
779    print_debug("You should pass the filename only (not the full path) to this function!");
780  }
781
782  $fsfilename = get_rrd_path($device, $filename);
783
784  if ($GLOBALS['config']['influxdb']['enabled'])
785  {
786    influxdb_update( $device, $filename, $options );
787  }
788
789  return rrdtool("update", $fsfilename, $options);
790}
791
792// DOCME needs phpdoc block
793// TESTME needs unit testing
794function rrdtool_fetch($filename, $options)
795{
796  return rrdtool('fetch', $filename, $options);
797}
798
799// TESTME needs unit testing
800/**
801 * Returns the UNIX timestamp of the most recent update of $filename
802 *
803 * @param string $filename RRD filename
804 * @param string $options Mostly not required
805 * @return string UNIX timestamp
806 */
807function rrdtool_last($filename, $options = '')
808{
809  return rrdtool('last', $filename, $options);
810}
811
812// TESTME needs unit testing
813/**
814 * Returns the UNIX timestamp and the value stored for each datum in the most recent update of $filename
815 *
816 * @param string $filename RRD filename
817 * @param string $options Mostly not required
818 * @return string UNIX timestamp and the value stored for each datum
819 */
820function rrdtool_lastupdate($filename, $options = '')
821{
822  return rrdtool('lastupdate', $filename, $options);
823}
824
825// TESTME needs unit testing
826/**
827 * Renames a DS inside an RRD file
828 *
829 * @param array  $device   Device
830 * @param string $filename Filename
831 * @param string $oldname  Current DS name
832 * @param string $newname  New DS name
833 */
834function rrdtool_rename_ds($device, $filename, $oldname, $newname)
835{
836  global $config;
837
838  $return = FALSE;
839  if ($config['norrd'])
840  {
841    print_message('[%gRRD Disabled%n] ');
842    return $return;
843  }
844
845  // rrdtool tune rename DS supported since v1.4
846  $version = get_versions();
847  if (version_compare($version['rrdtool_version'], '1.4', '>='))
848  {
849    $fsfilename = get_rrd_path($device, $filename);
850    print_debug("RRD DS renamed, file $fsfilename: '$oldname' -> '$newname'");
851    return rrdtool('tune', $filename, "--data-source-rename $oldname:$newname");
852  }
853
854  // Comparability with old version (but we support only >= v1.5.5, this not required)
855  if (OBS_RRD_NOLOCAL)
856  {
857    print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
858  } else {
859    $fsfilename = get_rrd_path($device, $filename);
860    if (is_file($fsfilename))
861    {
862      // this function used in discovery, where not exist rrd pipes
863      $command = $config['rrdtool'] . " tune $fsfilename --data-source-rename $oldname:$newname";
864      $return  = external_exec($command);
865      //print_vars($GLOBALS['exec_status']);
866      if ($GLOBALS['exec_status']['exitcode'] === 0)
867      {
868        print_debug("RRD DS renamed, file $fsfilename: '$oldname' -> '$newname'");
869      } else {
870        $return = FALSE;
871      }
872    }
873  }
874
875  return $return;
876}
877
878// TESTME needs unit testing
879/**
880 * Adds a DS to an RRD file
881 *
882 * @param array Device
883 * @param string Filename
884 * @param string New DS name
885 */
886function rrdtool_add_ds($device, $filename, $add)
887{
888  global $config;
889
890  $return = FALSE;
891  if ($config['norrd'])
892  {
893    print_message("[%gRRD Disabled%n] ");
894    return $return;
895  }
896
897  // rrdtool tune add DS supported since v1.4
898  $version = get_versions();
899  if (version_compare($version['rrdtool_version'], '1.4', '>='))
900  {
901    $fsfilename = get_rrd_path($device, $filename);
902    print_debug("RRD DS added, file ".$fsfilename.": '".$add."'");
903    return rrdtool('tune', $filename, "DS:$add");
904  }
905
906  // Comparability with old version (but we support only >= v1.5.5, this not required)
907  if (OBS_RRD_NOLOCAL)
908  {
909    print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
910  } else {
911    $fsfilename = get_rrd_path($device, $filename);
912    if (is_file($fsfilename))
913    {
914      // this function used in discovery, where not exist rrd pipes
915
916      $fsfilename = get_rrd_path($device, $filename);
917
918      $return  = external_exec($config['install_dir'] . "/scripts/add_ds_to_rrd.pl ".dirname($fsfilename)." ".basename($fsfilename)." $add");
919
920      //print_vars($GLOBALS['exec_status']);
921      if ($GLOBALS['exec_status']['exitcode'] === 0)
922      {
923        print_debug("RRD DS added, file ".$fsfilename.": '".$add."'");
924      } else {
925        $return = FALSE;
926      }
927    }
928  }
929
930  return $return;
931}
932
933// TESTME needs unit testing
934/**
935 * Adds one or more RRAs to an RRD file; space-separated if you want to add more than one.
936 *
937 * @param array  Device
938 * @param string Filename
939 * @param array  RRA(s) to be added to the RRD file
940 */
941function rrdtool_add_rra($device, $filename, $options)
942{
943  global $config;
944
945  if ($config['norrd'])
946  {
947    print_message('[%gRRD Disabled%n] ');
948  }
949  else if (OBS_RRD_NOLOCAL)
950  {
951    ///FIXME Currently unsupported on remote rrdcached
952    print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
953  } else {
954    $fsfilename = get_rrd_path($device, $filename);
955
956    external_exec($config['install_dir'] . "/scripts/rrdtoolx.py addrra $fsfilename $fsfilename.new $options");
957    rename("$fsfilename.new", $fsfilename);
958  }
959}
960
961/**
962 * Escapes strings for RRDtool
963 *
964 * @param string String to escape
965 * @param integer if passed, string will be padded and trimmed to exactly this length (after rrdtool unescapes it)
966 *
967 * @return string Escaped string
968 */
969// TESTME needs unit testing
970function rrdtool_escape($string, $maxlength = NULL)
971{
972  if ($maxlength != NULL)
973  {
974    $string = substr(str_pad($string, $maxlength),0,$maxlength);
975  }
976
977  $string = str_replace(array(':', "'", '%'), array('\:', '`', '%%'), $string);
978
979  // FIXME: should maybe also probably escape these? # \ ? [ ^ ] ( $ ) '
980
981  return $string;
982}
983
984/**
985 * Helper function to strip quotes from RRD output
986 *
987 * @str RRD-Info generated string
988 * @return String with one surrounding pair of quotes stripped
989 */
990// TESTME needs unit testing
991function rrd_strip_quotes($str)
992{
993  if ($str[0] == '"' && $str[strlen($str)-1] == '"')
994  {
995    return substr($str, 1, strlen($str)-2);
996  }
997
998  return $str;
999}
1000
1001/**
1002 * Determine useful information about RRD file
1003 *
1004 * Copyright (C) 2009  Bruno Prémont <bonbons AT linux-vserver.org>
1005 *
1006 * @file Name of RRD file to analyse
1007 *
1008 * @return Array describing the RRD file
1009 *
1010 */
1011// TESTME needs unit testing
1012function rrdtool_file_info($file)
1013{
1014  global $config;
1015
1016  $info = array('filename'=>$file);
1017
1018  if (OBS_RRD_NOLOCAL)
1019  {
1020    ///FIXME Currently unsupported on remote rrdcached
1021    print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
1022    return $info;
1023  }
1024
1025  $rrd = array_filter(explode(PHP_EOL, external_exec($config['rrdtool'] . ' info ' . $file)), 'strlen');
1026  if ($rrd)
1027  {
1028    foreach ($rrd as $s)
1029    {
1030      $p = strpos($s, '=');
1031      if ($p === false)
1032      {
1033        continue;
1034      }
1035
1036      $key = trim(substr($s, 0, $p));
1037      $value = trim(substr($s, $p+1));
1038      if (strncmp($key,'ds[', 3) == 0)
1039      {
1040        /* DS definition */
1041        $p = strpos($key, ']');
1042        $ds = substr($key, 3, $p-3);
1043        if (!isset($info['DS']))
1044        {
1045          $info['DS'] = array();
1046        }
1047
1048        $ds_key = substr($key, $p+2);
1049
1050        if (strpos($ds_key, '[') === false)
1051        {
1052          if (!isset($info['DS']["$ds"]))
1053          {
1054            $info['DS']["$ds"] = array();
1055          }
1056          $info['DS']["$ds"]["$ds_key"] = rrd_strip_quotes($value);
1057        }
1058      }
1059      else if (strncmp($key, 'rra[', 4) == 0)
1060      {
1061        /* RRD definition */
1062        $p = strpos($key, ']');
1063        $rra = substr($key, 4, $p-4);
1064        if (!isset($info['RRA']))
1065        {
1066          $info['RRA'] = array();
1067        }
1068        $rra_key = substr($key, $p+2);
1069
1070        if (strpos($rra_key, '[') === false)
1071        {
1072          if (!isset($info['RRA']["$rra"]))
1073          {
1074            $info['RRA']["$rra"] = array();
1075          }
1076          $info['RRA']["$rra"]["$rra_key"] = rrd_strip_quotes($value);
1077        }
1078      } else if (strpos($key, '[') === false) {
1079        $info[$key] = rrd_strip_quotes($value);
1080      }
1081    }
1082  }
1083
1084  return $info;
1085}
1086
1087// Creates a string of X number of ,ADDNAN. Used when aggregating things.
1088function rrd_addnan($count)
1089{
1090  return str_repeat(',ADDNAN', $count);
1091}
1092
1093// creates an rpn string to add an array of DSes together
1094function rrd_aggregate_dses($ds_list)
1095{
1096  return implode(',', $ds_list) . rrd_addnan(count($ds_list) - 1);
1097}
1098
1099// EOF
1100