mirror of
https://github.com/librenms/librenms.git
synced 2024-09-21 10:28:13 +00:00
Feature: Custom OID polling and graphing (#10945)
* merge * fix db migration * fix new auth * fix new auth * fix new auth * fix new auth * fix db schema tests * fix polling customoid * fix polling customoid * fix graph * fix graph * fix graph * fix CI * fix CI * always update prev value * typo
This commit is contained in:
parent
a1f4b1b88f
commit
934260cc75
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateCustomoidsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('customoids', function (Blueprint $table) {
|
||||
$table->increments('customoid_id');
|
||||
$table->unsignedInteger('device_id')->default(0);
|
||||
$table->string('customoid_descr', 255)->nullable()->default('');
|
||||
$table->tinyInteger('customoid_deleted')->default(0);
|
||||
$table->double('customoid_current')->nullable();
|
||||
$table->double('customoid_prev')->nullable();
|
||||
$table->string('customoid_oid', 255)->nullable();
|
||||
$table->string('customoid_datatype', 20)->default('GAUGE');
|
||||
$table->string('customoid_unit', 20)->nullable();
|
||||
$table->unsignedInteger('customoid_divisor')->default(1);
|
||||
$table->unsignedInteger('customoid_multiplier')->default(1);
|
||||
$table->double('customoid_limit')->nullable();
|
||||
$table->double('customoid_limit_warn')->nullable();
|
||||
$table->double('customoid_limit_low')->nullable();
|
||||
$table->double('customoid_limit_low_warn')->nullable();
|
||||
$table->tinyInteger('customoid_alert')->default(0);
|
||||
$table->tinyInteger('customoid_passed')->default(0);
|
||||
$table->timestamp('lastupdate')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
|
||||
$table->string('user_func', 100)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('customoids');
|
||||
}
|
||||
}
|
@ -861,6 +861,13 @@ function is_custom_graph($type, $subtype, $device)
|
||||
return false;
|
||||
} // is_custom_graph
|
||||
|
||||
function is_customoid_graph($type, $subtype)
|
||||
{
|
||||
if (!empty($subtype) && $type == 'customoid') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} // is_customoid_graph
|
||||
|
||||
/*
|
||||
* FIXME: Dummy implementation
|
||||
@ -1569,6 +1576,22 @@ function fahrenheit_to_celsius($value, $scale = 'fahrenheit')
|
||||
return sprintf('%.02f', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts celsius to fahrenheit (with 2 decimal places)
|
||||
* if $scale is not celsius, it assumes celsius and returns the value
|
||||
*
|
||||
* @param float $value
|
||||
* @param string $scale fahrenheit or celsius
|
||||
* @return string (containing a float)
|
||||
*/
|
||||
function celsius_to_fahrenheit($value, $scale = 'celsius')
|
||||
{
|
||||
if ($scale === 'celsius') {
|
||||
$value = ($value * 1.8) + 32;
|
||||
}
|
||||
return sprintf('%.02f', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts uW to dBm
|
||||
* $value must be positive
|
||||
|
167
includes/html/forms/customoid.inc.php
Normal file
167
includes/html/forms/customoid.inc.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
header('Content-type: application/json');
|
||||
|
||||
if (!Auth::user()->hasGlobalAdmin()) {
|
||||
$response = array(
|
||||
'status' => 'error',
|
||||
'message' => 'Need to be admin',
|
||||
);
|
||||
echo _json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$status = 'ok';
|
||||
$message = '';
|
||||
|
||||
$device_id = $_POST['device_id'];
|
||||
$id = $_POST['ccustomoid_id'];
|
||||
$action = mres($_POST['action']);
|
||||
$name = mres($_POST['name']);
|
||||
$oid = mres($_POST['oid']);
|
||||
$datatype = mres($_POST['datatype']);
|
||||
if (empty(mres($_POST['unit']))) {
|
||||
$unit = array('NULL');
|
||||
} else {
|
||||
$unit = mres($_POST['unit']);
|
||||
}
|
||||
if (!empty(mres($_POST['limit'])) && is_numeric(mres($_POST['limit']))) {
|
||||
$limit = mres($_POST['limit']);
|
||||
} else {
|
||||
$limit = array('NULL');
|
||||
}
|
||||
if (!empty(mres($_POST['limit_warn'])) && is_numeric(mres($_POST['limit_warn']))) {
|
||||
$limit_warn = mres($_POST['limit_warn']);
|
||||
} else {
|
||||
$limit_warn = array('NULL');
|
||||
}
|
||||
if (!empty(mres($_POST['limit_low'])) && is_numeric(mres($_POST['limit_low']))) {
|
||||
$limit_low = mres($_POST['limit_low']);
|
||||
} else {
|
||||
$limit_low = array('NULL');
|
||||
}
|
||||
if (!empty(mres($_POST['limit_low_warn'])) && is_numeric(mres($_POST['limit_low_warn']))) {
|
||||
$limit_low_warn = mres($_POST['limit_low_warn']);
|
||||
} else {
|
||||
$limit_low_warn = array('NULL');
|
||||
}
|
||||
if (mres($_POST['alerts']) == 'on') {
|
||||
$alerts = 1;
|
||||
} else {
|
||||
$alerts = 0;
|
||||
}
|
||||
if (mres($_POST['passed']) == 'on') {
|
||||
$passed = 1;
|
||||
} else {
|
||||
$passed = 0;
|
||||
}
|
||||
if (!empty(mres($_POST['divisor'])) && is_numeric(mres($_POST['divisor']))) {
|
||||
$divisor = mres($_POST['divisor']);
|
||||
} else {
|
||||
$divisor = 1;
|
||||
}
|
||||
if (!empty(mres($_POST['multiplier'])) && is_numeric(mres($_POST['multiplier']))) {
|
||||
$multiplier = mres($_POST['multiplier']);
|
||||
} else {
|
||||
$multiplier = 1;
|
||||
}
|
||||
if (!empty(mres($_POST['user_func']))) {
|
||||
$user_func = mres($_POST['user_func']);
|
||||
} else {
|
||||
$user_func = array('NULL');
|
||||
}
|
||||
|
||||
if ($action == "test") {
|
||||
$query = "SELECT * FROM `devices` WHERE `device_id` = $device_id LIMIT 1";
|
||||
$device = dbFetchRow($query);
|
||||
|
||||
$rawdata = snmp_get($device, $oid, '-Oqv');
|
||||
|
||||
if (is_numeric($rawdata)) {
|
||||
if (dbUpdate(
|
||||
array(
|
||||
'customoid_passed' => 1,
|
||||
),
|
||||
'customoids',
|
||||
'customoid_id=?',
|
||||
array($id)
|
||||
) >= 0) {
|
||||
$message = "Test successful for <i>$name</i>, value $rawdata received";
|
||||
} else {
|
||||
$status = 'error';
|
||||
$message = "Failed to set pass on OID <i>$name</i>";
|
||||
}
|
||||
} else {
|
||||
$status = 'error';
|
||||
$message = "Invalid data in SNMP reply, value $rawdata received";
|
||||
}
|
||||
} else {
|
||||
if (is_numeric($id) && $id > 0) {
|
||||
if (dbUpdate(
|
||||
array(
|
||||
'customoid_descr' => $name,
|
||||
'customoid_oid' => $oid,
|
||||
'customoid_datatype' => $datatype,
|
||||
'customoid_unit' => $unit,
|
||||
'customoid_divisor' => $divisor,
|
||||
'customoid_multiplier' => $multiplier,
|
||||
'customoid_limit' => $limit,
|
||||
'customoid_limit_warn' => $limit_warn,
|
||||
'customoid_limit_low' => $limit_low,
|
||||
'customoid_limit_low_warn' => $limit_low_warn,
|
||||
'customoid_alert' => $alerts,
|
||||
'customoid_passed' => $passed,
|
||||
'user_func' => $user_func
|
||||
),
|
||||
'customoids',
|
||||
"`customoid_id` = ?",
|
||||
array($id)
|
||||
) >= 0) { //end if condition
|
||||
$message = "Edited OID: <i>$name</i>";
|
||||
} else {
|
||||
$status = 'error';
|
||||
$message = "Failed to edit OID <i>$name</i>";
|
||||
}
|
||||
} else {
|
||||
if (empty($name)) {
|
||||
$status = 'error';
|
||||
$message = 'No OID name provided';
|
||||
} else {
|
||||
if (dbFetchCell('SELECT 1 FROM `customoids` WHERE `customoid_descr` = ? AND `device_id`=?', array($name, $device_id))) {
|
||||
$status = 'error';
|
||||
$message = "OID named <i>$name</i> on this device already exists";
|
||||
} else {
|
||||
$id = dbInsert(
|
||||
array(
|
||||
'device_id' => $device_id,
|
||||
'customoid_descr' => $name,
|
||||
'customoid_oid' => $oid,
|
||||
'customoid_datatype' => $datatype,
|
||||
'customoid_unit' => $unit,
|
||||
'customoid_divisor' => $divisor,
|
||||
'customoid_multiplier' => $multiplier,
|
||||
'customoid_limit' => $limit,
|
||||
'customoid_limit_warn' => $limit_warn,
|
||||
'customoid_limit_low' => $limit_low,
|
||||
'customoid_limit_low_warn' => $limit_low_warn,
|
||||
'customoid_alert' => $alerts,
|
||||
'customoid_passed' => $passed,
|
||||
'user_func' => $user_func
|
||||
),
|
||||
'customoids'
|
||||
);
|
||||
if ($id) {
|
||||
$message = "Added OID: <i>$name</i>";
|
||||
} else {
|
||||
$status = 'error';
|
||||
$message = "Failed to add OID: <i>$name</i>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
die(json_encode([
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
]));
|
25
includes/html/forms/delete-customoid.inc.php
Normal file
25
includes/html/forms/delete-customoid.inc.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
header('Content-type: text/plain');
|
||||
|
||||
if (!Auth::user()->hasGlobalAdmin()) {
|
||||
$response = array(
|
||||
'status' => 'error',
|
||||
'message' => 'Need to be admin',
|
||||
);
|
||||
echo _json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_numeric($_POST['customoid_id'])) {
|
||||
echo 'ERROR: No alert selected';
|
||||
exit;
|
||||
} else {
|
||||
if (dbDelete('customoids', '`customoid_id` = ?', array($_POST['customoid_id']))) {
|
||||
echo 'Custom OID has been deleted.';
|
||||
exit;
|
||||
} else {
|
||||
echo 'ERROR: Custom OID has not been deleted.';
|
||||
exit;
|
||||
}
|
||||
}
|
46
includes/html/forms/parse-customoid.inc.php
Normal file
46
includes/html/forms/parse-customoid.inc.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
if (!Auth::user()->hasGlobalAdmin()) {
|
||||
$response = array(
|
||||
'status' => 'error',
|
||||
'message' => 'Need to be admin',
|
||||
);
|
||||
echo _json_encode($response);
|
||||
exit;
|
||||
}
|
||||
$customoid_id = $_POST['customoid_id'];
|
||||
|
||||
if (is_numeric($customoid_id) && $customoid_id > 0) {
|
||||
$oid = dbFetchRow('SELECT * FROM `customoids` WHERE `customoid_id` = ? LIMIT 1', [$customoid_id]);
|
||||
|
||||
if ($oid['customoid_alert'] == 1) {
|
||||
$alerts = true;
|
||||
} else {
|
||||
$alerts = false;
|
||||
}
|
||||
if ($oid['customoid_passed'] == 1) {
|
||||
$cpassed = true;
|
||||
$passed = 'on';
|
||||
} else {
|
||||
$cpassed = false;
|
||||
$passed = '';
|
||||
}
|
||||
|
||||
header('Content-type: application/json');
|
||||
echo json_encode([
|
||||
'name' => $oid['customoid_descr'],
|
||||
'oid' => $oid['customoid_oid'],
|
||||
'datatype' => $oid['customoid_datatype'],
|
||||
'unit' => $oid['customoid_unit'],
|
||||
'divisor' => $oid['customoid_divisor'],
|
||||
'multiplier' => $oid['customoid_multiplier'],
|
||||
'limit' => $oid['customoid_limit'],
|
||||
'limit_warn' => $oid['customoid_limit_warn'],
|
||||
'limit_low' => $oid['customoid_limit_low'],
|
||||
'limit_low_warn' => $oid['customoid_limit_low_warn'],
|
||||
'alerts' => $alerts,
|
||||
'cpassed' => $cpassed,
|
||||
'passed' => $passed,
|
||||
'user_func' => $oid['user_func'],
|
||||
]);
|
||||
}
|
7
includes/html/graphs/customoid/auth.inc.php
Normal file
7
includes/html/graphs/customoid/auth.inc.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
if ($auth || device_permitted($device['device_id'])) {
|
||||
$title = generate_device_link($device);
|
||||
$title .= ' :: Custom OID ';
|
||||
$auth = true;
|
||||
}
|
18
includes/html/graphs/customoid/customoid.inc.php
Normal file
18
includes/html/graphs/customoid/customoid.inc.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
$rrd_filename = rrd_name($device['hostname'], array($type, $subtype));
|
||||
|
||||
require 'includes/html/graphs/common.inc.php';
|
||||
|
||||
$scale_min = 0;
|
||||
$graph_max = 1;
|
||||
$unit_text = $unit;
|
||||
|
||||
$ds = 'oid_value';
|
||||
|
||||
$colour_area = '9999cc';
|
||||
$colour_line = '0000cc';
|
||||
|
||||
$colour_area_max = '9999cc';
|
||||
|
||||
require 'includes/html/graphs/generic_simplex.inc.php';
|
@ -39,6 +39,9 @@ $graph_title = format_hostname($device);
|
||||
|
||||
if ($auth && is_custom_graph($type, $subtype, $device)) {
|
||||
include(Config::get('install_dir') . "/includes/html/graphs/custom.inc.php");
|
||||
} elseif ($auth && is_customoid_graph($type, $subtype)) {
|
||||
$unit = $vars['unit'];
|
||||
include(Config::get('install_dir') . "/includes/html/graphs/customoid/customoid.inc.php");
|
||||
} elseif ($auth && is_mib_graph($type, $subtype)) {
|
||||
include Config::get('install_dir') . "/includes/html/graphs/$type/mib.inc.php";
|
||||
} elseif ($auth && is_file(Config::get('install_dir') . "/includes/html/graphs/$type/$subtype.inc.php")) {
|
||||
|
58
includes/html/modal/delete_customoid.inc.php
Normal file
58
includes/html/modal/delete_customoid.inc.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
if (!(Auth::user()->hasGlobalAdmin())) {
|
||||
die('ERROR: You need to be admin');
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal fade" id="delete-oid-form" tabindex="-1" role="dialog" aria-labelledby="Delete" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h5 class="modal-title" id="Delete">Confirm Delete</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>If you would like to remove this OID then please click Delete.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form role="form" class="remove_oid_form">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger danger" id="delete-oid-button" data-target="delete-oid-button">Delete</button>
|
||||
<input type="hidden" name="dcustomoid_id" id="dcustomoid_id" value="">
|
||||
<input type="hidden" name="confirm" id="confirm" value="yes">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#delete-oid-form').on('show.bs.modal', function(event) {
|
||||
customoid_id = $(event.relatedTarget).data('customoid_id');
|
||||
$("#dcustomoid_id").val(customoid_id);
|
||||
});
|
||||
|
||||
$('#delete-oid-button').click('', function(event) {
|
||||
event.preventDefault();
|
||||
var customoid_id = $("#dcustomoid_id").val();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: 'ajax_form.php',
|
||||
data: { type: "delete-customoid", customoid_id: customoid_id },
|
||||
dataType: "html",
|
||||
success: function(msg) {
|
||||
if(msg.indexOf("ERROR:") <= -1) {
|
||||
$("#row_"+customoid_id).remove();
|
||||
}
|
||||
$("#message").html('<div class="alert alert-info">'+msg+'</div>');
|
||||
$("#delete-oid-form").modal('hide');
|
||||
},
|
||||
error: function() {
|
||||
$("#message").html('<div class="alert alert-info">This OID could not be deleted.</div>');
|
||||
$("#delete-oid-form").modal('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
226
includes/html/modal/new_customoid.inc.php
Normal file
226
includes/html/modal/new_customoid.inc.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
if (!(Auth::user()->hasGlobalAdmin())) {
|
||||
die('ERROR: You need to be admin');
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal fade" id="create-oid-form" tabindex="-1" role="dialog" aria-labelledby="Create" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h5 class="modal-title" id="Create">Custom OID :: <a href="https://docs.librenms.org/">Docs <i class="fa fa-book fa-1x"></i></a> </h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" role="form" id="coids" class="form-horizontal coid_form">
|
||||
<input type="hidden" name="device_id" id="device_id" value="<?php echo isset($device['device_id']) ? $device['device_id'] : -1; ?>">
|
||||
<input type="hidden" name="device_name" id="device_name" value="<?php echo format_hostname($device); ?>">
|
||||
<input type="hidden" name="ccustomoid_id" id="ccustomoid_id" value="">
|
||||
<input type="hidden" name="type" id="type" value="customoid">
|
||||
<input type="hidden" name="action" id="action" value="">
|
||||
<div class='form-group' title="A description of the OID">
|
||||
<label for='name' class='col-sm-4 col-md-3 control-label'>Name: </label>
|
||||
<div class='col-sm-8 col-md-9'>
|
||||
<input type='text' id='name' name='name' class='form-control validation' maxlength='200' required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" title="SNMP OID">
|
||||
<label for='oid' class='col-sm-4 col-md-3 control-label'>OID: </label>
|
||||
<div class='col-sm-8 col-md-9'>
|
||||
<input type='text' id='oid' name='oid' class='form-control validation' maxlength='255' required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" title="SNMP data type">
|
||||
<label for='datatype' class='col-sm-4 col-md-3 control-label'>Data Type: </label>
|
||||
<div class='col-sm-8 col-md-9'>
|
||||
<select class="form-control" id="datatype" name="datatype">
|
||||
<option value="COUNTER">COUNTER</option>
|
||||
<option value="GAUGE">GAUGE</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" title="Unit of value being polled">
|
||||
<label for='unit' class='col-sm-4 col-md-3 control-label'>Unit: </label>
|
||||
<div class='col-sm-8 col-md-9'>
|
||||
<input type='text' id='unit' name='unit' class='form-control validation' maxlength='10'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-group form-inline'>
|
||||
<label class='col-sm-4 col-md-3 control-label'>Calculations: </label>
|
||||
<div class="col-sm-8">
|
||||
<label for='divisor' class='col-sm-4 col-md-3 control-label' title="Divide raw SNMP value by">Divisor: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Divide raw SNMP value by">
|
||||
<input type='text' id='divisor' name='divisor' class='form-control' size="4">
|
||||
</div>
|
||||
<label for='multiplier' class='col-sm-4 col-md-3 control-label' title="Multiply raw SNMP value by">Multiplier: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Multiply raw SNMP value by">
|
||||
<input type='text' id='multiplier' name='multiplier' class='form-control' size="4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" title="User function to apply to value">
|
||||
<label for='user_func' class='col-sm-4 col-md-3 control-label'>User Function: </label>
|
||||
<div class='col-sm-8 col-md-9'>
|
||||
<select class="form-control" id="user_func" name="user_func">
|
||||
<option value=""></option>
|
||||
<option value="celsius_to_fahrenheit">C to F</option>
|
||||
<option value="fahrenheit_to_celsius">F to C</option>
|
||||
<option value="uw_to_dbm">uW to dBm</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-group form-inline'>
|
||||
<label class='col-sm-4 col-md-3 control-label'>Alert Thresholds: </label>
|
||||
<div class="col-sm-8">
|
||||
<label for='limit' class='col-sm-4 col-md-3 control-label' title="Level to alert above">High: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Level to alert above">
|
||||
<input type='text' id='limit' name='limit' class='form-control' size="4">
|
||||
</div>
|
||||
<label for='limit_low' class='col-sm-4 col-md-3 control-label' title="Level to alert below">Low: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Level to alert below">
|
||||
<input type='text' id='limit_low' name='limit_low' class='form-control' size="4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-group form-inline'>
|
||||
<label class='col-sm-4 col-md-3 control-label'>Warning Thresholds: </label>
|
||||
<div class="col-sm-8">
|
||||
<label for='limit_warn' class='col-sm-4 col-md-3 control-label' title="Level to warn above">High: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Level to warn above">
|
||||
<input type='text' id='limit_warn' name='limit_warn' class='form-control' size="4">
|
||||
</div>
|
||||
<label for='limit_low_warn' class='col-sm-4 col-md-3 control-label' title="Level to warn below">Low: </label>
|
||||
<div class="col-sm-4 col-md-3" title="Level to warn below">
|
||||
<input type='text' id='limit_low_warn' name='limit_low_warn' class='form-control' size="4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" title="Alerts for this OID enabled">
|
||||
<label for='alerts' class='col-sm-4 col-md-3 control-label'>Alerts Enabled: </label>
|
||||
<div class='col-sm-4 col-md-3'>
|
||||
<input type='checkbox' name='alerts' id='alerts'>
|
||||
</div>
|
||||
<label for='passed' class='col-sm-4 col-md-3 control-label'>Passed Check: </label>
|
||||
<div class='col-sm-4 col-md-3'>
|
||||
<input type='checkbox' name='cpassed' id='cpassed' disabled>
|
||||
<input type='hidden' name='passed' id='passed' value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-center">
|
||||
<button type="button" class="btn btn-success" id="save-oid-button" name="save-oid-button">
|
||||
Save OID
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="test-oid-button" name="test-oid-button">
|
||||
Test OID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-group form-inline'>
|
||||
<div class="col-sm-12">
|
||||
<p><small><em>OID will not be polled until a test is successfully complete.</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#create-oid-form').on('show.bs.modal', function(e) {
|
||||
var customoid_id = $(e.relatedTarget).data('customoid_id');
|
||||
$('#ccustomoid_id').val(customoid_id);
|
||||
if (customoid_id >= 0) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "ajax_form.php",
|
||||
data: { type: "parse-customoid", customoid_id: customoid_id },
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
$('#name').val(data.name);
|
||||
$('#oid').val(data.oid);
|
||||
$('#datatype').val(data.datatype);
|
||||
$('#datatype').prop('disabled', true);
|
||||
$('#unit').val(data.unit);
|
||||
$('#divisor').val(data.divisor);
|
||||
$('#multiplier').val(data.multiplier);
|
||||
$('#user_func').val(data.user_func);
|
||||
$('#limit').val(data.limit);
|
||||
$('#limit_warn').val(data.limit_warn);
|
||||
$('#limit_low').val(data.limit_low);
|
||||
$('#limit_low_warn').val(data.limit_low_warn);
|
||||
$('#alerts').prop('checked', data.alerts);
|
||||
$('#passed').val(data.passed);
|
||||
$('#cpassed').prop('checked', data.passed);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('#name').val('');
|
||||
$('#oid').val('');
|
||||
$('#datatype').val('GAUGE');
|
||||
$('#datatype').prop('disabled', false);
|
||||
$('#unit').val('');
|
||||
$('#divisor').val('');
|
||||
$('#multiplier').val('');
|
||||
$('#user_func').val('');
|
||||
$('#limit').val('');
|
||||
$('#limit_warn').val('');
|
||||
$('#limit_low').val('');
|
||||
$('#limit_low_warn').val('');
|
||||
$('#alerts').prop('checked', false);
|
||||
$('#passed').val('');
|
||||
$('#cpassed').prop('checked', false);
|
||||
}
|
||||
});
|
||||
$('#save-oid-button').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#action').val('save');
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "ajax_form.php",
|
||||
data: $('form.coid_form').serializeArray(),
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.status == 'ok') {
|
||||
toastr.success(data.message);
|
||||
$('#create-oid-form').modal('hide');
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastr.error(data.message);
|
||||
}
|
||||
},
|
||||
error: function (exception) {
|
||||
toastr.error('Failed to process OID');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#test-oid-button').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#action').val('test');
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "ajax_form.php",
|
||||
data: $('form.coid_form').serializeArray(),
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.status == 'ok') {
|
||||
toastr.success(data.message);
|
||||
$("#passed").val('on');
|
||||
$("#cpassed").prop("checked", true);
|
||||
} else {
|
||||
toastr.error(data.message);
|
||||
}
|
||||
},
|
||||
error: function (exception) {
|
||||
toastr.error('Failed to process OID');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#oid').change(function () {
|
||||
$("#passed").val('');
|
||||
$("#cpassed").prop("checked", false);
|
||||
});
|
||||
</script>
|
@ -55,6 +55,8 @@ if (!Auth::user()->hasGlobalAdmin()) {
|
||||
|
||||
$panes['component'] = 'Components';
|
||||
|
||||
$panes['customoid'] = 'Custom OID';
|
||||
|
||||
print_optionbar_start();
|
||||
|
||||
unset($sep);
|
||||
|
4
includes/html/pages/device/edit/customoid.inc.php
Normal file
4
includes/html/pages/device/edit/customoid.inc.php
Normal file
@ -0,0 +1,4 @@
|
||||
<h3> Custom OIDs </h3>
|
||||
<?php
|
||||
|
||||
require_once 'includes/html/print-customoid.php';
|
@ -37,7 +37,11 @@ foreach ($graph_enable as $section => $nothing) {
|
||||
echo '<span class="pagemenu-selected">';
|
||||
}
|
||||
|
||||
echo generate_link(ucwords($type), $link_array, array('group' => $type));
|
||||
if ($type == 'customoid') {
|
||||
echo generate_link(ucwords('Custom OID'), $link_array, array('group' => $type));
|
||||
} else {
|
||||
echo generate_link(ucwords($type), $link_array, array('group' => $type));
|
||||
}
|
||||
if ($vars['group'] == $type) {
|
||||
echo '</span>';
|
||||
}
|
||||
@ -55,10 +59,22 @@ $graph_enable = $graph_enable[$vars['group']];
|
||||
foreach ($graph_enable as $graph => $entry) {
|
||||
$graph_array = array();
|
||||
if ($graph_enable[$graph]) {
|
||||
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr");
|
||||
$graph_array['type'] = 'device_'.$graph;
|
||||
|
||||
include 'includes/html/print-device-graph.php';
|
||||
if ($graph == 'customoid') {
|
||||
foreach (dbFetchRows('SELECT * FROM `customoids` WHERE `device_id` = ? ORDER BY `customoid_descr`', array($device['device_id'])) as $graph_entry) {
|
||||
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr").": ".$graph_entry['customoid_descr'];
|
||||
$graph_array['type'] = 'customoid_' . $graph_entry['customoid_descr'];
|
||||
if (!empty($graph_entry['customoid_unit'])) {
|
||||
$graph_array['unit'] = $graph_entry['customoid_unit'];
|
||||
} else {
|
||||
$graph_array['unit'] = 'value';
|
||||
}
|
||||
include 'includes/html/print-device-graph.php';
|
||||
}
|
||||
} else {
|
||||
$graph_title = \LibreNMS\Config::get("graph_types.device.$graph.descr");
|
||||
$graph_array['type'] = 'device_'.$graph;
|
||||
include 'includes/html/print-device-graph.php';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
31
includes/html/pages/device/graphs/customoid.inc.php
Normal file
31
includes/html/pages/device/graphs/customoid.inc.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
$row = 1;
|
||||
|
||||
foreach (dbFetchRows('SELECT * FROM `customoids` WHERE `device_id` = ? ORDER BY `customoid_descr`', array($device['device_id'])) as $customoid) {
|
||||
if (!is_integer($row / 2)) {
|
||||
$row_colour = Config::get("list_colour.even");
|
||||
} else {
|
||||
$row_colour = Config::get("list_colour.odd");
|
||||
}
|
||||
$customoid_descr = $customoid['customoid_descr'];
|
||||
$customoid_unit = $customoid['customoid_unit'];
|
||||
$customoid_current = format_si($customoid['customoid_current']).$customoid_unit;
|
||||
$customoid_limit = format_si($customoid['customoid_limit']).$customoid_unit;
|
||||
$customoid_limit_low = format_si($customoid['$customoid_limit_low']).$customoid_unit;
|
||||
echo "<div class='panel panel-default'>
|
||||
<div class='panel-heading'>
|
||||
<h3 class='panel-title'>$customoid_descr <div class='pull-right'>$customoid_current | $customoid_limit_low <> $customoid_limit</div></h3>
|
||||
</div>";
|
||||
echo "<div class='panel-body'>";
|
||||
|
||||
$graph_array['id'] = $customoid['customoid_id'];
|
||||
$graph_array['title'] = $customoid['customoid_descr'];
|
||||
$graph_array['type'] = 'customoid';
|
||||
|
||||
include 'includes/html/print-graphrow.inc.php';
|
||||
|
||||
echo '</div></div>';
|
||||
|
||||
$row++;
|
||||
}
|
140
includes/html/print-customoid.php
Normal file
140
includes/html/print-customoid.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
use LibreNMS\Authentication\LegacyAuth;
|
||||
|
||||
require_once 'includes/html/modal/new_customoid.inc.php';
|
||||
require_once 'includes/html/modal/delete_customoid.inc.php';
|
||||
|
||||
$no_refresh = true;
|
||||
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<span id="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="" id="oid_form">
|
||||
|
||||
<?php
|
||||
|
||||
if (isset($_POST['num_of_rows']) && $_POST['num_of_rows'] > 0) {
|
||||
$rows = $_POST['rows'];
|
||||
} else {
|
||||
$rows = 10;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed" width="100%">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>OID</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
<th colspan="2">Alert Threshold</th>
|
||||
<th colspan="2">Warning Threshold</th>
|
||||
<th>Alerts</th>
|
||||
<th>Passed</th>
|
||||
<th style="width:86px;">Action</th>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
echo '<tr>
|
||||
<td colspan="4">
|
||||
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#create-oid-form" data-device_id="'.$device['device_id'].'"'.(Auth::user()->hasGlobalAdmin() ? "" : " disabled").'><i class="fa fa-plus"></i> Add New OID</button>
|
||||
</td>
|
||||
<th><small>High</small></th>
|
||||
<th><small>Low</small></th>
|
||||
<th><small>High</small></th>
|
||||
<th><small>Low</small></th>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><select name="rows" id="rows" class="form-control input-sm" onChange="updateResults(this);">';
|
||||
|
||||
$num_of_rows_options = array(
|
||||
'10',
|
||||
'25',
|
||||
'50',
|
||||
'100',
|
||||
);
|
||||
foreach ($num_of_rows_options as $option) {
|
||||
echo "<option value='".$option."'".($rows == $option ? " selected" : "").">".$option."</option>";
|
||||
}
|
||||
|
||||
echo '</select></td>
|
||||
</tr>';
|
||||
|
||||
$query = 'FROM customoids';
|
||||
$where = '';
|
||||
$param = [];
|
||||
if (isset($device['device_id']) && $device['device_id'] > 0) {
|
||||
$where = 'WHERE (device_id=?)';
|
||||
$param[] = $device['device_id'];
|
||||
}
|
||||
|
||||
$count = dbFetchCell("SELECT COUNT(*) $query $where", $param);
|
||||
if (isset($_POST['page_num']) && $_POST['page_num'] > 0 && $_POST['page_num'] <= $count) {
|
||||
$page_num = $_POST['page_num'];
|
||||
} else {
|
||||
$page_num = 1;
|
||||
}
|
||||
|
||||
$start = (($page_num - 1) * $rows);
|
||||
$full_query = "SELECT * $query $where ORDER BY customoid_descr ASC LIMIT $start,$rows";
|
||||
|
||||
foreach (dbFetchRows($full_query, $param) as $oid) {
|
||||
echo "<tr class='".$oid['customoid_id']."' id='row_".$oid['customoid_id']."'>";
|
||||
echo '<td>'.$oid['customoid_descr'].'</td>';
|
||||
echo '<td>'.$oid['customoid_oid'].'</td>';
|
||||
echo '<td>'.$oid['customoid_current'].'</td>';
|
||||
echo '<td>'.$oid['customoid_unit'].'</td>';
|
||||
echo '<td>'.$oid['customoid_limit'].'</td>';
|
||||
echo '<td>'.$oid['customoid_limit_low'].'</td>';
|
||||
echo '<td>'.$oid['customoid_limit_warn'].'</td>';
|
||||
echo '<td>'.$oid['customoid_limit_low_warn'].'</td>';
|
||||
echo "<td><input id='".$oid['customoid_id']."' type='checkbox' name='alert'".($oid['customoid_alert'] ? " checked" : "")." disabled></td>";
|
||||
echo "<td><input id='".$oid['customoid_id']."' type='checkbox' name='passed'".($oid['customoid_passed'] ? " checked" : "")." disabled></td>";
|
||||
echo '<td>';
|
||||
echo "<div class='btn-group btn-group-sm' role='group'>";
|
||||
echo "<button type='button' class='btn btn-primary' data-toggle='modal' data-target='#create-oid-form' data-customoid_id='".$oid['customoid_id']."' name='edit-oid' data-content='' data-container='body'".(Auth::user()->hasGlobalAdmin() ? "" : " disabled")."><i class='fa fa-lg fa-pencil' aria-hidden='true'></i></button>";
|
||||
echo "<button type='button' class='btn btn-danger' aria-label='Delete' data-toggle='modal' data-target='#delete-oid-form' data-customoid_id='".$oid['customoid_id']."' name='delete-oid' data-content='' data-container='body'><i class='fa fa-lg fa-trash' aria-hidden='true'".(Auth::user()->hasGlobalAdmin() ? "" : " disabled")."></i></button>";
|
||||
echo "</div>";
|
||||
echo '</td>';
|
||||
echo "</tr>\r\n";
|
||||
}//end foreach
|
||||
|
||||
if (($count % $rows) > 0) {
|
||||
echo '<tr>
|
||||
<td colspan="11" align="center">'.generate_pagination($count, $rows, $page_num).'</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
echo '</table>
|
||||
</div>
|
||||
<input type="hidden" name="page_num" id="page_num" value="'.$page_num.'">
|
||||
<input type="hidden" name="num_of_rows" id="num_of_rows" value="'.$rows.'">
|
||||
</form>';
|
||||
|
||||
?>
|
||||
|
||||
<script>
|
||||
|
||||
$("[data-toggle='modal'], [data-toggle='popover']").popover({
|
||||
trigger: 'hover',
|
||||
'placement': 'top'
|
||||
});
|
||||
|
||||
function updateResults(rows) {
|
||||
$('#num_of_rows').val(rows.value);
|
||||
$('#page_num').val(1);
|
||||
$('#oid_form').submit();
|
||||
}
|
||||
|
||||
function changePage(page,e) {
|
||||
e.preventDefault();
|
||||
$('#page_num').val(page);
|
||||
$('#oid_form').submit();
|
||||
}
|
||||
|
||||
</script>
|
59
includes/polling/customoid.inc.php
Normal file
59
includes/polling/customoid.inc.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use LibreNMS\RRD\RrdDefinition;
|
||||
|
||||
foreach (dbFetchRows("SELECT * FROM `customoids` WHERE `customoid_passed` = 1 AND `device_id` = ?", array($device['device_id'])) as $customoid) {
|
||||
d_echo($customoid);
|
||||
|
||||
$prev_oid_value = $customoid['customoid_current'];
|
||||
|
||||
$rawdata = snmp_get($device, $customoid['customoid_oid'], '-Oqv');
|
||||
|
||||
$user_funcs = array(
|
||||
"celsius_to_fahrenheit",
|
||||
"fahrenheit_to_celsius",
|
||||
"uw_to_dbm"
|
||||
);
|
||||
|
||||
if (is_numeric($rawdata)) {
|
||||
$graphs['customoid'] = true;
|
||||
$oid_value = $rawdata;
|
||||
} else {
|
||||
$oid_value = 0;
|
||||
$error = "Invalid SNMP reply.";
|
||||
}
|
||||
|
||||
if ($customoid['customoid_divisor'] && $oid_value !== 0) {
|
||||
$oid_value = ($oid_value / $customoid['customoid_divisor']);
|
||||
}
|
||||
if ($customoid['customoid_multiplier']) {
|
||||
$oid_value = ($oid_value * $customoid['customoid_multiplier']);
|
||||
}
|
||||
|
||||
if (isset($customoid['user_func']) && in_array($customoid['user_func'], $user_funcs)) {
|
||||
$oid_value = $customoid['user_func']($oid_value);
|
||||
}
|
||||
|
||||
echo 'Custom OID '.$customoid['customoid_descr'].': ';
|
||||
echo $oid_value.' '.$customoid['customoid_unit']."\n";
|
||||
|
||||
$fields = array(
|
||||
'oid' => $oid_value,
|
||||
);
|
||||
|
||||
$rrd_name = array('customoid', $customoid['customoid_descr']);
|
||||
if ($customoid['customoid_datatype'] == 'COUNTER') {
|
||||
$datatype = $customoid['customoid_datatype'];
|
||||
} else {
|
||||
$datatype = 'GAUGE';
|
||||
}
|
||||
$rrd_def = RrdDefinition::make()
|
||||
->addDataset('oid_value', $datatype);
|
||||
|
||||
$tags = compact('rrd_name', 'rrd_def');
|
||||
|
||||
data_update($device, 'customoid', $tags, $fields);
|
||||
dbUpdate(array('customoid_current' => $oid_value, 'lastupdate' => array('NOW()'), 'customoid_prev' => $prev_oid_value), 'customoids', '`customoid_id` = ?', array($customoid['customoid_id']));
|
||||
}//end foreach
|
||||
|
||||
unset($customoid, $prev_oid_value, $rawdata, $user_funcs, $oid_value, $error, $fields, $rrd_def, $rrd_name, $tags);
|
@ -411,5 +411,27 @@
|
||||
{
|
||||
"rule": "%applications.app_type='portactivity' && %applications_metrics.imaps_total_from>'0'",
|
||||
"name": "IRCD Connections From"
|
||||
},
|
||||
{
|
||||
"rule": "customoids.customoid_current >= `customoids.customoid_limit` && customoids.customoid_alert = \"1\" && macros.device_up = \"1\"",
|
||||
"name": "CustomOID over limit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"rule": "customoids.customoid_current <= `customoids.customoid_limit_low` && customoids.customoid_alert = \"1\" && macros.device_up = \"1\"",
|
||||
"name": "CustomOID under limit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"rule": "customoids.customoid_current >= `customoids.customoid_limit_warn` && customoids.customoid_alert = \"1\" && macros.device_up = \"1\"",
|
||||
"name": "CustomOID over warning limit",
|
||||
"severity": "warning",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"rule": "customoids.customoid_current <= `customoids.customoid_limit_low_warn` && customoids.customoid_alert = \"1\" && macros.device_up = \"1\"",
|
||||
"name": "CustomOID under warning limit",
|
||||
"severity": "warning",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
|
@ -1988,6 +1988,14 @@
|
||||
},
|
||||
"type": "graph"
|
||||
},
|
||||
"graph_types.device.customoid": {
|
||||
"default": {
|
||||
"section": "customoid",
|
||||
"order": 0,
|
||||
"descr": "Custom OID"
|
||||
},
|
||||
"type": "graph"
|
||||
},
|
||||
"graph_types.device.fdb_count": {
|
||||
"default": {
|
||||
"section": "system",
|
||||
@ -3669,6 +3677,11 @@
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
|
||||
},
|
||||
"poller_modules.customoid": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
|
||||
},
|
||||
"poller_modules.bgp-peers": {
|
||||
"default": true,
|
||||
|
@ -411,6 +411,29 @@ customers:
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [customer_id], Unique: true, Type: BTREE }
|
||||
username: { Name: username, Columns: [username], Unique: true, Type: BTREE }
|
||||
customoids:
|
||||
Columns:
|
||||
- { Field: customoid_id, Type: 'int(10) unsigned', 'Null': false, Extra: auto_increment }
|
||||
- { Field: device_id, Type: 'int(10) unsigned', 'Null': false, Extra: '', Default: '0' }
|
||||
- { Field: customoid_descr, Type: varchar(255), 'Null': true, Extra: '', Default: '' }
|
||||
- { Field: customoid_deleted, Type: tinyint(4), 'Null': false, Extra: '', Default: '0' }
|
||||
- { Field: customoid_current, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_prev, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_oid, Type: varchar(255), 'Null': true, Extra: '' }
|
||||
- { Field: customoid_datatype, Type: varchar(20), 'Null': false, Extra: '', Default: GAUGE }
|
||||
- { Field: customoid_unit, Type: varchar(20), 'Null': true, Extra: '' }
|
||||
- { Field: customoid_divisor, Type: 'int(10) unsigned', 'Null': false, Extra: '', Default: '1' }
|
||||
- { Field: customoid_multiplier, Type: 'int(10) unsigned', 'Null': false, Extra: '', Default: '1' }
|
||||
- { Field: customoid_limit, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_limit_warn, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_limit_low, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_limit_low_warn, Type: double, 'Null': true, Extra: '' }
|
||||
- { Field: customoid_alert, Type: tinyint(4), 'Null': false, Extra: '', Default: '0' }
|
||||
- { Field: customoid_passed, Type: tinyint(4), 'Null': false, Extra: '', Default: '0' }
|
||||
- { Field: lastupdate, Type: timestamp, 'Null': false, Extra: 'on update CURRENT_TIMESTAMP', Default: CURRENT_TIMESTAMP }
|
||||
- { Field: user_func, Type: varchar(100), 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [customoid_id], Unique: true, Type: BTREE }
|
||||
dashboards:
|
||||
Columns:
|
||||
- { Field: dashboard_id, Type: 'int(10) unsigned', 'Null': false, Extra: auto_increment }
|
||||
|
Loading…
Reference in New Issue
Block a user