mirror of
https://github.com/stylersnico/librenms.git
synced 2026-07-12 16:06:25 +02:00
Merge pull request #1273 from paulgear/mib-based-polling
MIB-based polling
This commit is contained in:
@@ -5,6 +5,7 @@ find . \
|
||||
-path ./html/includes/geshi -prune -o \
|
||||
-path ./html/includes/jpgraph -prune -o \
|
||||
-path ./html/js/jqplot -prune -o \
|
||||
-path ./junk -prune -o \
|
||||
-path ./logs -prune -o \
|
||||
-path ./mibs -prune -o \
|
||||
-path ./rrd -prune -o \
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
## WARNING ##
|
||||
|
||||
MIB-based polling is experimental. It might overload your LibreNMS server,
|
||||
destroy your data, set your routers on fire, and kick your cat. It has been
|
||||
tested against a very limited set of devices (namely Ruckus ZD1000 wireless
|
||||
controllers, and Net-SNMP on Linux). It may fail badly on other hardware.
|
||||
|
||||
The approach taken is fairly basic and I claim no special expertise in
|
||||
understanding MIBs. Most of my understanding of SNMP comes from reading
|
||||
net-snmp man pages, and reverse engineering the output of snmptranslate and
|
||||
snmpwalk and trying to make devices work with LibreNMS. I may have made
|
||||
false assumptions and probably use wrong terminology in many places. Feel
|
||||
free to offer corrections/suggestions via pull requests or email.
|
||||
|
||||
Paul Gear <paul@librenms.org>
|
||||
|
||||
## Overview ##
|
||||
|
||||
MIB-based polling is disabled by default; you must set
|
||||
`$config['poller_modules']['mib'] = 1;`
|
||||
in config.php to enable it.
|
||||
|
||||
The components involved in of MIB-based support are:
|
||||
|
||||
### Discovery ###
|
||||
|
||||
- MIB-based detection is not involved; any work done here would have to be
|
||||
duplicated by the poller and thus would only increase load.
|
||||
|
||||
### Polling ###
|
||||
|
||||
- The file `includes/snmp.inc.php` now contains code which can parse MIBs
|
||||
using `snmptranslate` and use the data returned to populate an array
|
||||
which guides the poller in what to store. At the moment, only OIDs with
|
||||
Unsigned32 and Counter64 data types are parsed.
|
||||
- `includes/polling/mib.inc.php` looks for a MIB matching sysObjectID in
|
||||
the MIB directory; if one is found, it:
|
||||
- parses it
|
||||
- walks that MIB on the device
|
||||
- stores any numeric results in individual RRD files
|
||||
- updates/adds graph definitions in the previously-unused graph_types
|
||||
database table
|
||||
- Individual OSes (`includes/polling/os/*.inc.php`) can poll extra MIBs
|
||||
for a given OS by calling poll_mib(). At the moment, this actually
|
||||
happens before the general MIB polling.
|
||||
- Devices may be excluded from MIB polling by changing the setting in the
|
||||
device edit screen (`/device/device=ID/tab=edit/section=modules/`)
|
||||
|
||||
### Graphing ###
|
||||
|
||||
- For each graph type defined in the database, a graph will appear in:
|
||||
Device -> Graphs -> MIB
|
||||
- MIB graphs are generated generically by
|
||||
`html/includes/graphs/device/mib.inc.php`
|
||||
- At the moment, all units are placed in the same graph. This is probably
|
||||
non-optimal for, e.g., wifi controllers with hundreds of APs attached.
|
||||
|
||||
## Adding/testing other device types ##
|
||||
|
||||
One of the goals of this work is to help take out the heavy lifting of
|
||||
adding new device types. Even if you want fully customised graphs or
|
||||
tables, you can use the automatic collection of MIBs to make it easy to
|
||||
gather the data you want.
|
||||
|
||||
### How to add a new device MIB ###
|
||||
|
||||
1. Ensure the manufacturer's MIB is present in the mibs directory. If you
|
||||
plan to submit your work to LibreNMS, make sure you attribute the source
|
||||
of the MIB, including the exact download URL.
|
||||
2. Check that `snmptranslate -Ts -M mibs -m MODULE | grep mibName` produces
|
||||
a named list of OIDs. See the comments for `snmp_mib_walk()` in
|
||||
`includes/snmp.inc.php` for an example.
|
||||
3. Check that `snmptranslate -Td -On -M mibs -m MODULE MODULE::mibName`
|
||||
produces a parsed description of the OID values. An example can be
|
||||
found in the comments for `snmp_mib_parse()` in `includes/snmp.inc.php`.
|
||||
4. Get the `sysObjectID` from a device, for example:```
|
||||
snmpget -v2c -c public -OUsb -m SNMPv2-MIB -M /opt/librenms/mibs -t 30 hostname sysObjectID.0
|
||||
```
|
||||
5. Ensure `snmptranslate -m all -M /opt/librenms/mibs OID 2>/dev/null`
|
||||
(where OID is the value returned for sysObjectID above) results in a
|
||||
valid name for the MIB. See the comments for `snmp_translate()` in
|
||||
`includes/snmp.inc.php` for an example. If this step fails, it means
|
||||
there is something wrong with the MIB and net-snmp cannot parse it.
|
||||
6. Add any additional MIBs you wish to poll for specific device types to
|
||||
`includes/polling/os/OSNAME.inc.php` by calling `poll_mibs()` with the
|
||||
MIB module and name. See includes/polling/os/ruckuswireless.inc.php for
|
||||
an example.
|
||||
7. That should be all you need to see MIB graphs!
|
||||
|
||||
## TODO ##
|
||||
|
||||
- Save the most recent MIB data in the database (including string types
|
||||
which cannot be graphed). Display it in the appropriate places.
|
||||
- Parse and save integer and timetick data types.
|
||||
- Filter MIBs/OIDs from being polled and/or saved.
|
||||
- Move graphs from the MIB section to elsewhere. e.g. There is already
|
||||
specific support for wireless APs - this should be utilised, but isn't
|
||||
yet.
|
||||
- Combine multiple MIB values into graphs automatically on a predefined or
|
||||
user-defined basis.
|
||||
- Include MIB types in stats submissions.
|
||||
|
||||
+2
-1
@@ -103,7 +103,8 @@ To go into a bit more detail, the following are usually needed:
|
||||
**includes/definitions.inc.php**
|
||||
Update this file to include the required definitions for the new OS.
|
||||
**includes/discovery/os/ciscowlc.inc.php**
|
||||
This file just sets the $os variable, done by checking the sysDescr snmp value for a particular value that matches the OS you are adding.
|
||||
This file just sets the $os variable, done by checking the SNMP tree for a particular value that matches the OS you are adding. Typically, this will come from the presence of specific values in
|
||||
sysObjectID or sysDescr, or the existence of a particular enterprise tree.
|
||||
**includes/polling/os/ciscowlc.inc.php**
|
||||
This file will usually set the variables for $version and $hardware gained from an snmp lookup.
|
||||
**html/images/os/$os.png**
|
||||
|
||||
@@ -70,6 +70,7 @@ $config['poller_modules']['aruba-controller'] = 1;
|
||||
$config['poller_modules']['entity-physical'] = 1;
|
||||
$config['poller_modules']['applications'] = 1;
|
||||
$config['poller_modules']['cisco-asa-firewall'] = 1;
|
||||
$config['poller_modules']['mib'] = 0;
|
||||
```
|
||||
|
||||
#### Poller modules
|
||||
@@ -112,7 +113,7 @@ $config['poller_modules']['cisco-asa-firewall'] = 1;
|
||||
|
||||
`ospf`: OSPF Support.
|
||||
|
||||
`cisco-ipsec-flow-monitor': IPSec statistics support.
|
||||
`cisco-ipsec-flow-monitor`: IPSec statistics support.
|
||||
|
||||
`cisco-remote-access-monitor`: Cisco remote access support.
|
||||
|
||||
@@ -138,6 +139,8 @@ $config['poller_modules']['cisco-asa-firewall'] = 1;
|
||||
|
||||
`cisco-asa-firewall`: Cisco ASA firewall support.
|
||||
|
||||
`mib`: Support for generic MIB parsing.
|
||||
|
||||
#### Running
|
||||
|
||||
Here are some examples of running poller from within your install directory.
|
||||
@@ -176,4 +179,4 @@ DB Updates
|
||||
|
||||
RRD Updates
|
||||
|
||||
SNMP Response
|
||||
SNMP Response
|
||||
|
||||
@@ -436,6 +436,7 @@ function get_graphs() {
|
||||
$router = $app->router()->getCurrentRoute()->getParams();
|
||||
$hostname = $router['hostname'];
|
||||
|
||||
// FIXME: this has some overlap with html/pages/device/graphs.inc.php
|
||||
// use hostname as device_id if it's all digits
|
||||
$device_id = ctype_digit($hostname) ? $hostname : getidbyname($hostname);
|
||||
$graphs = array();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/*
|
||||
* LibreNMS front page graphs
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2013 Gear Consulting Pty Ltd <http://libertysys.com.au/>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* LibreNMS front page top devices graph
|
||||
* - Find most utilised devices that have been polled in the last N minutes
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2013 Gear Consulting Pty Ltd <http://libertysys.com.au/>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* LibreNMS front page top ports graph
|
||||
* - Find most utilised ports that have been polled in the last N minutes
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2013 Gear Consulting Pty Ltd <http://libertysys.com.au/>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
if ($auth || device_permitted($device['device_id']))
|
||||
{
|
||||
$title = generate_device_link($device);
|
||||
$graph_title = $device['hostname'];
|
||||
$auth = TRUE;
|
||||
$title = generate_device_link($device);
|
||||
$graph_title = $device['hostname'];
|
||||
$auth = true;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*
|
||||
* LibreNMS MIB-based polling
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2015 Gear Consulting Pty Ltd <github@libertysys.com.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version. Please see LICENSE.txt at the top level of
|
||||
* the source code distribution for details.
|
||||
*/
|
||||
|
||||
$rrd_list = array();
|
||||
$prefix = rrd_name($device['hostname'], array($subtype, ""), "");
|
||||
foreach (glob($prefix."*.rrd") as $filename) {
|
||||
// find out what * expanded to
|
||||
$globpart = str_replace($prefix, '', $filename); // take off the prefix
|
||||
$instance = substr($globpart, 0, -4); // take off ".rrd"
|
||||
|
||||
$ds = array();
|
||||
$mibparts = explode("-", $subtype);
|
||||
$mibvar = end($mibparts);
|
||||
$ds['ds'] = name_shorten($mibvar);
|
||||
$ds['descr'] = "$mibvar-$instance";
|
||||
$ds['filename'] = $filename;
|
||||
$rrd_list[] = $ds;
|
||||
}
|
||||
|
||||
$colours = 'mixed';
|
||||
$scale_min = "0";
|
||||
$nototal = 0;
|
||||
$simple_rrd = true;
|
||||
|
||||
include("includes/graphs/generic_multi_line.inc.php");
|
||||
@@ -38,37 +38,16 @@ $graphfile = $config['temp_dir'] . "/" . strgen() . ".png";
|
||||
$type = $graphtype['type'];
|
||||
$subtype = $graphtype['subtype'];
|
||||
|
||||
if (is_file($config['install_dir'] . "/html/includes/graphs/$type/$subtype.inc.php"))
|
||||
{
|
||||
$auth = is_client_authorized($_SERVER['REMOTE_ADDR']);
|
||||
include($config['install_dir'] . "/html/includes/graphs/$type/auth.inc.php");
|
||||
|
||||
if (isset($config['allow_unauth_graphs']) && $config['allow_unauth_graphs'])
|
||||
{
|
||||
$auth = "1"; // hardcode auth for all with config function
|
||||
}
|
||||
|
||||
if (isset($config['allow_unauth_graphs_cidr']) && count($config['allow_unauth_graphs_cidr']) > 0)
|
||||
{
|
||||
foreach ($config['allow_unauth_graphs_cidr'] as $range)
|
||||
{
|
||||
if (Net_IPv4::ipInNetwork($_SERVER['REMOTE_ADDR'], $range))
|
||||
{
|
||||
$auth = "1";
|
||||
if ($debug) { echo("matched $range"); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include($config['install_dir'] . "/html/includes/graphs/$type/auth.inc.php");
|
||||
|
||||
if (isset($auth) && $auth)
|
||||
{
|
||||
if ($auth === true && is_file($config['install_dir'] . "/html/includes/graphs/$type/$subtype.inc.php")) {
|
||||
include($config['install_dir'] . "/html/includes/graphs/$type/$subtype.inc.php");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
elseif ($auth === true && is_mib_graph($type, $subtype)) {
|
||||
include($config['install_dir'] . "/html/includes/graphs/$type/mib.inc.php");
|
||||
}
|
||||
else {
|
||||
graph_error("$type*$subtype ");//Graph Template Missing");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
// Sections are printed in the order they exist in $config['graph_sections']
|
||||
// Graphs are printed in the order they exist in $config['graph_types']
|
||||
|
||||
$link_array = array('page' => 'device',
|
||||
@@ -15,19 +14,19 @@ print_optionbar_start();
|
||||
|
||||
echo("<span style='font-weight: bold;'>Graphs</span> » ");
|
||||
|
||||
$sep = "";
|
||||
|
||||
foreach (dbFetchRows("SELECT * FROM device_graphs WHERE device_id = ? ORDER BY graph", array($device['device_id'])) as $graph)
|
||||
{
|
||||
$section = $config['graph_types']['device'][$graph['graph']]['section'];
|
||||
$graph_enable[$section][$graph['graph']] = $graph['graph'];
|
||||
if ($section != "") {
|
||||
$graph_enable[$section][$graph['graph']] = $graph['graph'];
|
||||
}
|
||||
}
|
||||
|
||||
// These are standard graphs we should have for all systems
|
||||
$graph_enable['poller']['poller_perf'] = 'device_poller_perf';
|
||||
$graph_enable['poller']['ping_perf'] = 'device_ping_perf';
|
||||
|
||||
#foreach ($config['graph_sections'] as $section)
|
||||
$sep = "";
|
||||
foreach ($graph_enable as $section => $nothing)
|
||||
{
|
||||
if (isset($graph_enable) && is_array($graph_enable[$section]))
|
||||
@@ -47,8 +46,8 @@ foreach ($graph_enable as $section => $nothing)
|
||||
$sep = " | ";
|
||||
}
|
||||
}
|
||||
|
||||
unset ($sep);
|
||||
|
||||
print_optionbar_end();
|
||||
|
||||
$graph_enable = $graph_enable[$vars['group']];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
// Sections are printed in the order they exist in $config['graph_sections']
|
||||
// Graphs are printed in the order they exist in $config['graph_types']
|
||||
|
||||
$link_array = array('page' => 'device',
|
||||
|
||||
@@ -45,20 +45,6 @@ if (!$auth)
|
||||
$title .= " :: ".ucfirst($subtype);
|
||||
}
|
||||
|
||||
# Load our list of available graphtypes for this object
|
||||
// FIXME not all of these are going to be valid
|
||||
if ($handle = opendir($config['install_dir'] . "/html/includes/graphs/".$type."/"))
|
||||
{
|
||||
while (false !== ($file = readdir($handle)))
|
||||
{
|
||||
if ($file != "." && $file != ".." && $file != "auth.inc.php" &&strstr($file, ".inc.php"))
|
||||
{
|
||||
$types[] = str_replace(".inc.php", "", $file);
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
|
||||
$graph_array = $vars;
|
||||
$graph_array['height'] = "60";
|
||||
$graph_array['width'] = $thumb_width;
|
||||
@@ -75,11 +61,14 @@ if (!$auth)
|
||||
onchange="window.open(this.options[this.selectedIndex].value,'_top')" >
|
||||
<?php
|
||||
|
||||
foreach ($types as $avail_type)
|
||||
foreach (get_graph_subtypes($type) as $avail_type)
|
||||
{
|
||||
echo("<option value='".generate_url($vars, array('type' => $type."_".$avail_type, 'page' => "graphs"))."'");
|
||||
if ($avail_type == $subtype) { echo(" selected"); }
|
||||
echo(">".nicecase($avail_type)."</option>");
|
||||
if ($avail_type == $subtype) {
|
||||
echo(" selected");
|
||||
}
|
||||
$display_type = is_mib_graph($type, $avail_type) ? $avail_type : nicecase($avail_type);
|
||||
echo(">$display_type</option>");
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
+157
-2
@@ -1,6 +1,20 @@
|
||||
<?php
|
||||
|
||||
// Common Functions
|
||||
/*
|
||||
* LibreNMS - Common Functions
|
||||
*
|
||||
* Original Observium version by: Adam Armstrong, Tom Laermans
|
||||
* Copyright (c) 2009-2012 Adam Armstrong.
|
||||
*
|
||||
* Additions for LibreNMS by: Neil Lathwood, Paul Gear, Tim DuFrane
|
||||
* Copyright (c) 2014-2015 Neil Lathwood <http://www.lathwood.co.uk>
|
||||
* Copyright (c) 2014-2015 Gear Consulting Pty Ltd <http://libertysys.com.au/>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version. Please see LICENSE.txt at the top level of
|
||||
* the source code distribution for details.
|
||||
*/
|
||||
|
||||
function format_number_short($number, $sf)
|
||||
{
|
||||
@@ -476,6 +490,19 @@ function get_dev_attrib($device, $attrib_type)
|
||||
}
|
||||
}
|
||||
|
||||
function is_dev_attrib_enabled($device, $attrib, $default = true)
|
||||
{
|
||||
$val = get_dev_attrib($device, $attrib);
|
||||
if ($val != NULL) {
|
||||
// attribute is set
|
||||
return ($val != 0);
|
||||
}
|
||||
else {
|
||||
// attribute not set
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
function del_dev_attrib($device, $attrib_type)
|
||||
{
|
||||
return dbDelete('devices_attribs', "`device_id` = ? AND `attrib_type` = ?", array($device['device_id'], $attrib_type));
|
||||
@@ -592,4 +619,132 @@ function edit_service($service, $descr, $service_ip, $service_param = "", $servi
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* convenience function - please use this instead of 'if ($debug) { echo ...; }'
|
||||
*/
|
||||
function d_echo($text, $no_debug_text = null)
|
||||
{
|
||||
global $debug;
|
||||
if ($debug) {
|
||||
echo "$text";
|
||||
}
|
||||
elseif ($no_debug_text) {
|
||||
echo "$no_debug_text";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* convenience function - please use this instead of 'if ($debug) { print_r ...; }'
|
||||
*/
|
||||
function d_print_r($var, $no_debug_text = null)
|
||||
{
|
||||
global $debug;
|
||||
if ($debug) {
|
||||
print_r($var);
|
||||
}
|
||||
elseif ($no_debug_text) {
|
||||
echo "$no_debug_text";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Shorten the name so it works as an RRD data source name (limited to 19 chars by default).
|
||||
* Substitute for $subst if necessary.
|
||||
* @return the shortened name
|
||||
*/
|
||||
function name_shorten($name, $common = null, $subst = "mibval", $len = 19)
|
||||
{
|
||||
if ($common !== null) {
|
||||
// remove common from the beginning of the string, if present
|
||||
if (strlen($name) > $len && strpos($name, $common) >= 0) {
|
||||
$newname = str_replace($common, '', $name);
|
||||
$name = $newname;
|
||||
}
|
||||
}
|
||||
if (strlen($name) > $len) {
|
||||
$name = $subst;
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return the name of the rrd file for $host's $extra component
|
||||
* @param host Host name
|
||||
* @param extra Components of RRD filename - will be separated with "-"
|
||||
*/
|
||||
function rrd_name($host, $extra, $exten = ".rrd")
|
||||
{
|
||||
global $config;
|
||||
$filename = safename(is_array($extra) ? implode("-", $extra) : $extra);
|
||||
return implode("/", array($config['rrd_dir'], $host, $filename.$exten));
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if the given graph type is a dynamic MIB graph
|
||||
*/
|
||||
function is_mib_graph($type, $subtype)
|
||||
{
|
||||
global $config;
|
||||
return $config['graph_types'][$type][$subtype]['section'] == 'mib';
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if client IP address is authorized to access graphs
|
||||
*/
|
||||
function is_client_authorized($clientip)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if (isset($config['allow_unauth_graphs']) && $config['allow_unauth_graphs']) {
|
||||
d_echo("Unauthorized graphs allowed\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($config['allow_unauth_graphs_cidr'])) {
|
||||
foreach ($config['allow_unauth_graphs_cidr'] as $range) {
|
||||
if (Net_IPv4::ipInNetwork($clientip, $range)) {
|
||||
d_echo("Unauthorized graphs allowed from $range\n");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return an array of all graph subtypes for the given type
|
||||
* FIXME not all of these are going to be valid
|
||||
*/
|
||||
function get_graph_subtypes($type)
|
||||
{
|
||||
global $config;
|
||||
|
||||
$types = array();
|
||||
|
||||
// find the subtypes defined in files
|
||||
if ($handle = opendir($config['install_dir'] . "/html/includes/graphs/$type/")) {
|
||||
while (false !== ($file = readdir($handle))) {
|
||||
if ($file != "." && $file != ".." && $file != "auth.inc.php" && strstr($file, ".inc.php")) {
|
||||
$types[] = str_replace(".inc.php", "", $file);
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
|
||||
// find the MIB subtypes
|
||||
foreach ($config['graph_types'] as $type => $unused1) {
|
||||
print_r($type);
|
||||
foreach ($config['graph_types'][$type] as $subtype => $unused2) {
|
||||
print_r($subtype);
|
||||
if (is_mib_graph($type, $subtype)) {
|
||||
$types[] = $subtype;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort($types);
|
||||
return $types;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -533,6 +533,7 @@ $config['poller_modules']['aruba-controller'] = 1;
|
||||
$config['poller_modules']['entity-physical'] = 1;
|
||||
$config['poller_modules']['applications'] = 1;
|
||||
$config['poller_modules']['cisco-asa-firewall'] = 1;
|
||||
$config['poller_modules']['mib'] = 0;
|
||||
|
||||
// List of discovery modules. Need to be in this array to be
|
||||
// considered for execution.
|
||||
|
||||
@@ -1176,7 +1176,7 @@ foreach ($config['os'] as $this_os => $blah)
|
||||
|
||||
// Graph Types
|
||||
|
||||
$config['graph_sections'] = array('general', 'system', 'firewall', 'netstats', 'wireless', 'storage', 'vpdn', 'load balancer');
|
||||
include_once($config['install_dir'] . "/includes/load_db_graph_types.inc.php");
|
||||
|
||||
|
||||
// Device - Wireless - AirMAX
|
||||
|
||||
@@ -1247,23 +1247,3 @@ function ip_exists($ip) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* convenience function - please use this instead of 'if ($debug) { echo ...; }'
|
||||
*/
|
||||
function d_echo($text) {
|
||||
global $debug;
|
||||
if ($debug) {
|
||||
echo "$text\n";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* convenience function - please use this instead of 'if ($debug) { print_r ...; }'
|
||||
*/
|
||||
function d_print_r($var) {
|
||||
global $debug;
|
||||
if ($debug) {
|
||||
print_r($var);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/*
|
||||
* LibreNMS dynamic graph types
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2015 Gear Consulting Pty Ltd <github@libertysys.com.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version. Please see LICENSE.txt at the top level of
|
||||
* the source code distribution for details.
|
||||
*/
|
||||
|
||||
// load graph types from the database
|
||||
foreach (dbFetchRows('select * from graph_types') as $graph) {
|
||||
// remove leading 'graph_' from column names
|
||||
foreach ($graph as $k => $v) {
|
||||
$key = strpos($k, 'graph_') == 0 ? str_replace('graph_', '', $k) : $k;
|
||||
$g[$key] = $v;
|
||||
}
|
||||
$config['graph_types'][$g['type']][$g['subtype']] = $g;
|
||||
}
|
||||
@@ -183,6 +183,7 @@ function poll_device($device, $options)
|
||||
{
|
||||
if ($attribs['poll_'.$module] || ( $module_status && !isset($attribs['poll_'.$module])))
|
||||
{
|
||||
// TODO per-module polling stats
|
||||
include('includes/polling/'.$module.'.inc.php');
|
||||
} elseif (isset($attribs['poll_'.$module]) && $attribs['poll_'.$module] == "0") {
|
||||
echo("Module [ $module ] disabled on host.\n");
|
||||
@@ -201,11 +202,11 @@ function poll_device($device, $options)
|
||||
|
||||
foreach (dbFetch("SELECT `graph` FROM `device_graphs` WHERE `device_id` = ?", array($device['device_id'])) as $graph)
|
||||
{
|
||||
if (!isset($graphs[$graph["graph"]]))
|
||||
if (isset($graphs[$graph["graph"]]))
|
||||
{
|
||||
dbDelete('device_graphs', "`device_id` = ? AND `graph` = ?", array($device['device_id'], $graph["graph"]));
|
||||
} else {
|
||||
$oldgraphs[$graph["graph"]] = TRUE;
|
||||
} else {
|
||||
dbDelete('device_graphs', "`device_id` = ? AND `graph` = ?", array($device['device_id'], $graph["graph"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +223,7 @@ function poll_device($device, $options)
|
||||
|
||||
$device_end = utime(); $device_run = $device_end - $device_start; $device_time = substr($device_run, 0, 5);
|
||||
|
||||
// TODO: These should be easy converts to rrd_create_update()
|
||||
// Poller performance rrd
|
||||
$poller_rrd = $config['rrd_dir'] . "/" . $device['hostname'] . "/poller-perf.rrd";
|
||||
if (!is_file($poller_rrd))
|
||||
@@ -266,7 +268,7 @@ function poll_mib_def($device, $mib_name_table, $mib_subdir, $mib_oids, $mib_gra
|
||||
|
||||
global $config;
|
||||
|
||||
echo("This is mag_poll_mib_def Processing\n");
|
||||
echo("This is poll_mib_def Processing\n");
|
||||
$mib = NULL;
|
||||
|
||||
if (stristr($mib_name_table, "UBNT")) {
|
||||
@@ -346,4 +348,26 @@ function poll_mib_def($device, $mib_name_table, $mib_subdir, $mib_oids, $mib_gra
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Please use this instead of creating & updating RRD files manually.
|
||||
* @param device Device object - only 'hostname' is used at present
|
||||
* @param name Array of rrdname components
|
||||
* @param def Array of data definitions
|
||||
* @param val Array of value definitions
|
||||
*
|
||||
*/
|
||||
function rrd_create_update($device, $name, $def, $val, $step = 300)
|
||||
{
|
||||
global $config;
|
||||
$rrd = rrd_name($device['hostname'], $name);
|
||||
|
||||
if (!is_file($rrd) && $def != null) {
|
||||
// add the --step and the rra definitions to the array
|
||||
$newdef = "--step $step ".implode(' ', $def).$config['rrd_rra'];
|
||||
rrdtool_create($rrd, $newdef);
|
||||
}
|
||||
|
||||
rrdtool_update($rrd, $val);
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/*
|
||||
* LibreNMS MIB-based polling
|
||||
*
|
||||
* Author: Paul Gear
|
||||
* Copyright (c) 2015 Gear Consulting Pty Ltd <github@libertysys.com.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version. Please see LICENSE.txt at the top level of
|
||||
* the source code distribution for details.
|
||||
*/
|
||||
|
||||
$devicemib = array($device['sysObjectID'] => "all");
|
||||
poll_mibs($devicemib, $device, $graphs);
|
||||
@@ -2,8 +2,12 @@
|
||||
/*
|
||||
* LibreNMS Ruckus Wireless OS information module
|
||||
*
|
||||
* Originally by:
|
||||
* Copyright (c) 2015 Søren Friis Rosiak <sorenrosiak@gmail.com>
|
||||
*
|
||||
* Updates by Paul Gear:
|
||||
* Copyright (c) 2015 Gear Consulting Pty Ltd <github@libertysys.com.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
@@ -39,4 +43,12 @@ $ruckuscountry = first_oid_match($device, $ruckuscountries);
|
||||
if (isset($ruckuscountry) && $ruckuscountry != "") {
|
||||
$version .= " ($ruckuscountry)";
|
||||
}
|
||||
|
||||
$ruckus_mibs = array(
|
||||
"ruckusZDSystemStats" => "RUCKUS-ZD-SYSTEM-MIB",
|
||||
"ruckusZDWLANTable" => "RUCKUS-ZD-WLAN-MIB",
|
||||
"ruckusZDWLANAPTable" => "RUCKUS-ZD-WLAN-MIB",
|
||||
);
|
||||
poll_mibs($ruckus_mibs, $device, $graphs);
|
||||
|
||||
?>
|
||||
|
||||
@@ -113,6 +113,12 @@
|
||||
log_event("Contact -> ".$poll_device['sysContact'], $device, 'system');
|
||||
}
|
||||
|
||||
if ($poll_device['sysObjectID'] && $poll_device['sysObjectID'] != $device['sysObjectID'])
|
||||
{
|
||||
$update_array['sysObjectID'] = $poll_device['sysObjectID'];
|
||||
log_event("ObjectID -> ".$poll_device['sysObjectID'], $device, 'system');
|
||||
}
|
||||
|
||||
if ($poll_device['sysName'] && $poll_device['sysName'] != $device['sysName'])
|
||||
{
|
||||
$update_array['sysName'] = $poll_device['sysName'];
|
||||
|
||||
+300
-16
@@ -322,9 +322,9 @@ function snmp_cache_ifIndex($device)
|
||||
return $array;
|
||||
}
|
||||
|
||||
function snmpwalk_cache_oid($device, $oid, $array, $mib = NULL, $mibdir = NULL)
|
||||
function snmpwalk_cache_oid($device, $oid, $array, $mib = NULL, $mibdir = NULL, $snmpflags = "-OQUs")
|
||||
{
|
||||
$data = snmp_walk($device, $oid, "-OQUs", $mib, $mibdir);
|
||||
$data = snmp_walk($device, $oid, $snmpflags, $mib, $mibdir);
|
||||
foreach (explode("\n", $data) as $entry)
|
||||
{
|
||||
list($oid,$value) = explode("=", $entry, 2);
|
||||
@@ -345,20 +345,7 @@ function snmpwalk_cache_oid($device, $oid, $array, $mib = NULL, $mibdir = NULL)
|
||||
// to be the same.
|
||||
function snmpwalk_cache_oid_num($device, $oid, $array, $mib = NULL, $mibdir = NULL)
|
||||
{
|
||||
$data = snmp_walk($device, $oid, "-OQUn", $mib, $mibdir);
|
||||
|
||||
foreach (explode("\n", $data) as $entry)
|
||||
{
|
||||
list($oid,$value) = explode("=", $entry, 2);
|
||||
$oid = trim($oid); $value = trim($value);
|
||||
list($oid, $index) = explode(".", $oid, 2);
|
||||
if (!strstr($value, "at this OID") && isset($oid) && isset($index))
|
||||
{
|
||||
$array[$index][$oid] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
return snmpwalk_cache_oid($device, $oid, $array, $mib, $mibdir, $snmpflags = "-OQUn");
|
||||
}
|
||||
|
||||
|
||||
@@ -818,4 +805,301 @@ function snmp_gen_auth (&$device)
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
/*
|
||||
* Translate the given MIB into a vaguely useful PHP array. Each keyword becomes an array index.
|
||||
*
|
||||
* Example:
|
||||
* snmptranslate -Td -On -M mibs -m RUCKUS-ZD-SYSTEM-MIB RUCKUS-ZD-SYSTEM-MIB::ruckusZDSystemStatsNumSta
|
||||
* .1.3.6.1.4.1.25053.1.2.1.1.1.15.30
|
||||
* ruckusZDSystemStatsAllNumSta OBJECT-TYPE
|
||||
* -- FROM RUCKUS-ZD-SYSTEM-MIB
|
||||
* SYNTAX Unsigned32
|
||||
* MAX-ACCESS read-only
|
||||
* STATUS current
|
||||
* DESCRIPTION "Number of All client devices"
|
||||
* ::= { iso(1) org(3) dod(6) internet(1) private(4) enterprises(1) ruckusRootMIB(25053) ruckusObjects(1) ruckusZD(2) ruckusZDSystemModule(1) ruckusZDSystemMIB(1) ruckusZDSystemObjects(1)
|
||||
* ruckusZDSystemStats(15) 30 }
|
||||
*/
|
||||
function snmp_mib_parse($oid, $mib, $module, $mibdir = null)
|
||||
{
|
||||
global $debug;
|
||||
|
||||
$fulloid = explode(".", $oid);
|
||||
$lastpart = end($fulloid);
|
||||
|
||||
$cmd = "snmptranslate -Td -On";
|
||||
$cmd .= mibdir($mibdir);
|
||||
$cmd .= " -m ".$module." ".$module."::";
|
||||
$cmd .= $lastpart;
|
||||
|
||||
$result = array();
|
||||
$lines = preg_split('/\n+/', trim(shell_exec($cmd)));
|
||||
foreach ($lines as $l) {
|
||||
$f = preg_split('/\s+/', trim($l));
|
||||
// first line is all numeric
|
||||
if (preg_match('/^[\d.]+$/', $f[0])) {
|
||||
$result['oid'] = $f[0];
|
||||
continue;
|
||||
}
|
||||
// then the name of the object type
|
||||
if ($f[1] && $f[1] == "OBJECT-TYPE") {
|
||||
$result['object_type'] = $f[0];
|
||||
$result['shortname'] = str_replace($mib, '', $f[0]);
|
||||
$result['dsname'] = name_shorten($f[0], $mib);
|
||||
continue;
|
||||
}
|
||||
// then the other data elements
|
||||
if ($f[0] == "--" && $f[1] == "FROM") {
|
||||
$result['module'] = $f[2];
|
||||
continue;
|
||||
}
|
||||
if ($f[0] == "MAX-ACCESS") {
|
||||
$result['max_access'] = $f[1];
|
||||
continue;
|
||||
}
|
||||
if ($f[0] == "STATUS") {
|
||||
$result[strtolower($f[0])] = $f[1];
|
||||
continue;
|
||||
}
|
||||
if ($f[0] == "SYNTAX") {
|
||||
$result[strtolower($f[0])] = $f[1];
|
||||
continue;
|
||||
}
|
||||
if ($f[0] == "DESCRIPTION") {
|
||||
$desc = explode('"', $l);
|
||||
if ($desc[1]) {
|
||||
$str = preg_replace('/^[\s.]*/', '', $desc[1]);
|
||||
$str = preg_replace('/[\s.]*$/', '', $str);
|
||||
$result[strtolower($f[0])] = $str;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// The main mib entry doesn't have any useful data in it - only return items that have the syntax specified.
|
||||
if (isset($result['syntax']) && isset($result['object_type'])) {
|
||||
$result['mib'] = $mib;
|
||||
return $result;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Walks through the given MIB module, looking for the given MIB.
|
||||
* NOTE: different from snmp walk - this doesn't touch the device.
|
||||
* NOTE: There's probably a better way to do this with snmptranslate.
|
||||
*
|
||||
* Example:
|
||||
* snmptranslate -Ts -M mibs -m RUCKUS-ZD-SYSTEM-MIB | grep ruckusZDSystemStats
|
||||
* .iso.org.dod.internet.private.enterprises.ruckusRootMIB.ruckusObjects.ruckusZD.ruckusZDSystemModule.ruckusZDSystemMIB.ruckusZDSystemObjects.ruckusZDSystemStats
|
||||
* .iso.org.dod.internet.private.enterprises.ruckusRootMIB.ruckusObjects.ruckusZD.ruckusZDSystemModule.ruckusZDSystemMIB.ruckusZDSystemObjects.ruckusZDSystemStats.ruckusZDSystemStatsNumAP
|
||||
* .iso.org.dod.internet.private.enterprises.ruckusRootMIB.ruckusObjects.ruckusZD.ruckusZDSystemModule.ruckusZDSystemMIB.ruckusZDSystemObjects.ruckusZDSystemStats.ruckusZDSystemStatsNumSta
|
||||
* ...
|
||||
*/
|
||||
function snmp_mib_walk($mib, $module, $mibdir = null)
|
||||
{
|
||||
$cmd = "snmptranslate -Ts";
|
||||
$cmd .= mibdir($mibdir);
|
||||
$cmd .= " -m ".$module;
|
||||
$result = array();
|
||||
$data = preg_split('/\n+/', shell_exec($cmd));
|
||||
foreach ($data as $oid) {
|
||||
// only include oids which are part of this mib
|
||||
if (strstr($oid, $mib)) {
|
||||
$obj = snmp_mib_parse($oid, $mib, $module, $mibdir);
|
||||
if ($obj) {
|
||||
$result[] = $obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return an array containing all of the mib objects, keyed by object-type;
|
||||
* returns an empty array if something goes wrong.
|
||||
*/
|
||||
function snmp_mib_load($mib, $module, $mibdir = null)
|
||||
{
|
||||
$mibs = array();
|
||||
foreach (snmp_mib_walk($mib, $module, $mibdir) as $obj) {
|
||||
$mibs[$obj['object_type']] = $obj;
|
||||
}
|
||||
return $mibs;
|
||||
}
|
||||
|
||||
/*
|
||||
* Turn the given oid (name or numeric value) into a MODULE::mib name.
|
||||
* @return an array consisting of the module and mib names, or null if no matching MIB is found.
|
||||
* Example:
|
||||
* snmptranslate -m all -M mibs .1.3.6.1.4.1.8072.3.2.10 2>/dev/null
|
||||
* NET-SNMP-TC::linux
|
||||
*/
|
||||
function snmp_translate($oid, $module, $mibdir = null)
|
||||
{
|
||||
if ($module !== "all") {
|
||||
$oid = "$module::$oid";
|
||||
}
|
||||
$cmd = "snmptranslate" . mibdir($mibdir);
|
||||
$cmd .= " -m $module $oid"; // load all the MIBs looking for our object
|
||||
$cmd .= " 2>/dev/null"; // ignore invalid MIBs
|
||||
|
||||
$lines = preg_split('/\n+/', external_exec($cmd));
|
||||
if (empty($lines)) {
|
||||
d_echo("No results from snmptranslate\n");
|
||||
return null;
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
if (!preg_match('/(.*)::(.*)/', $lines[0], $matches)) {
|
||||
d_echo("This doesn't look like a MIB: $lines[0]\n");
|
||||
return null;
|
||||
}
|
||||
|
||||
d_echo("SNMP translated: $module::$oid -> $matches[1]::$matches[2]\n");
|
||||
return array($matches[1], $matches[2]);
|
||||
}
|
||||
|
||||
/*
|
||||
* check if the type of the oid is a numeric type, and if so,
|
||||
* @return the name of RRD type that is best suited to saving it
|
||||
*/
|
||||
function oid_rrd_type($oid, $mibdef) {
|
||||
if (!isset($mibdef[$oid])) {
|
||||
return false;
|
||||
}
|
||||
switch ($mibdef[$oid]['syntax']) {
|
||||
|
||||
case 'OCTET':
|
||||
case 'IpAddress':
|
||||
return false;
|
||||
|
||||
case 'TimeTicks':
|
||||
// Need to find a way to flag that this should be parsed
|
||||
//return 'COUNTER';
|
||||
return false;
|
||||
|
||||
case 'Counter64':
|
||||
return 'COUNTER:600:0:U';
|
||||
|
||||
case 'Unsigned32':
|
||||
return 'GAUGE:600:U:U';
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Construct a graph names for use in the database.
|
||||
* Tag each as in use on this device in &$graphs.
|
||||
* Update the database with graph definitions as needed.
|
||||
* We don't include the index in the graph name - that is handled at display time.
|
||||
*/
|
||||
function tag_graphs($mibname, $oids, $mibdef, &$graphs)
|
||||
{
|
||||
foreach ($oids as $index => $array) {
|
||||
foreach ($array as $oid => $val) {
|
||||
$graphname = $mibname."-".$mibdef[$oid]['shortname'];
|
||||
$graphs[$graphname] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure a graph_type definition exists in the database for the entities in this MIB
|
||||
*/
|
||||
function update_mib_graph_types($mibname, $oids, $mibdef, $graphs)
|
||||
{
|
||||
$seengraphs = array();
|
||||
|
||||
// Get the list of graphs currently in the database
|
||||
// FIXME: there's probably a more efficient way to do this
|
||||
foreach (dbFetch("SELECT DISTINCT `graph_subtype` FROM `graph_types` WHERE `graph_subtype` LIKE ?", array("$mibname-%")) as $graph) {
|
||||
$seengraphs[$graph['graph_subtype']] = true;
|
||||
}
|
||||
|
||||
foreach ($oids as $index => $array) {
|
||||
$i = 1;
|
||||
foreach ($array as $oid => $val) {
|
||||
$graphname = "$mibname-".$mibdef[$oid]['shortname'];
|
||||
|
||||
// add the graph if it's not in the database already
|
||||
if ($graphs[$graphname] && !$seengraphs[$graphname]) {
|
||||
// construct a graph definition based on the MIB definition
|
||||
$graphdef = array();
|
||||
$graphdef['graph_type'] = "device";
|
||||
$graphdef['graph_subtype'] = $graphname;
|
||||
$graphdef['graph_section'] = "mib";
|
||||
$graphdef['graph_descr'] = $mibdef[$oid]['description'];
|
||||
$graphdef['graph_order'] = $i++;
|
||||
// TODO: add colours, unit_text, and ds
|
||||
|
||||
// add graph to the database
|
||||
dbInsert($graphdef, 'graph_types');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Save all of the measurable oids for the device in their own RRDs.
|
||||
*/
|
||||
function save_mibs($device, $mibname, $oids, $mibdef, &$graphs)
|
||||
{
|
||||
$usedoids = array();
|
||||
foreach ($oids as $index => $array) {
|
||||
foreach ($array as $oid => $val) {
|
||||
$type = oid_rrd_type($oid, $mibdef);
|
||||
if ($type === false) {
|
||||
continue;
|
||||
}
|
||||
$usedoids[$index][$oid] = $val;
|
||||
rrd_create_update(
|
||||
$device,
|
||||
array($mibname, $mibdef[$oid]['shortname'], $index),
|
||||
array("DS:".$mibdef[$oid]['dsname'].":$type"),
|
||||
"N:$val"
|
||||
);
|
||||
}
|
||||
}
|
||||
tag_graphs($mibname, $usedoids, $mibdef, $graphs);
|
||||
update_mib_graph_types($mibname, $usedoids, $mibdef, $graphs);
|
||||
}
|
||||
|
||||
/*
|
||||
* Take a list of MIB name => module pairs.
|
||||
* Validate MIBs and poll based on the results.
|
||||
*/
|
||||
function poll_mibs($list, $device, &$graphs)
|
||||
{
|
||||
if (!is_dev_attrib_enabled($device, "poll_mib")) {
|
||||
d_echo("MIB module disabled for ".$device['hostname']."\n");
|
||||
return;
|
||||
}
|
||||
$mibdefs = array();
|
||||
echo("MIB-based polling:");
|
||||
d_echo("\n");
|
||||
|
||||
foreach ($list as $name => $module) {
|
||||
$translated = snmp_translate($name, $module);
|
||||
if ($translated) {
|
||||
echo(" $module::$name");
|
||||
d_echo("\n");
|
||||
$mod = $translated[0];
|
||||
$nam = $translated[1];
|
||||
$mibdefs[$nam] = snmp_mib_load($nam, $mod);
|
||||
$oids = snmpwalk_cache_oid($device, $nam, array(), $mod, null, "-OQUsb");
|
||||
d_print_r($oids);
|
||||
save_mibs($device, $nam, $oids, $mibdefs[$nam], $graphs);
|
||||
}
|
||||
else {
|
||||
d_echo("MIB: no match for $module::$name\n");
|
||||
}
|
||||
}
|
||||
d_echo("Done MIB-based polling");
|
||||
echo("\n");
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `graph_types` CHANGE `graph_subtype` `graph_subtype` varchar(64);
|
||||
ALTER TABLE `device_graphs` CHANGE `graph` `graph` varchar(64);
|
||||
ALTER TABLE `graph_types` CHANGE `graph_descr` `graph_descr` varchar(255);
|
||||
ALTER TABLE `graph_types` ADD PRIMARY KEY (`graph_type`, `graph_subtype`, `graph_section`);
|
||||
Reference in New Issue
Block a user