Device groups rewrite (#10346)

* Device Groups rewrite
Updated web ui
Static or dynamic groups allowed
Alert rule query builder
Translation support
Permissions support

* cleanup, make relationship save, and validate it

* builder WIP

* rules builder and rules saving/loading

* Parse query builder to Laravel Fluent query

* Upgrade existing groups when editing.
Properly update only dynamic groups when polling.

* remove unused old code
Update API and other places to use Eloquent

* debug output in poller restored

* Fix up some things
creating static
improved validation
fix js error on creation
Fix static groups in polling

* hide pattern for static group

* Implement authorization
Use in the menu too

* update schema

* fix rollback

* Don't abort on invalid queries

* fixes to query builder

* add test data, looks like macros aren't handled (omitted them because groups don't use them generally)

* Add macro support for QueryBuilderFluentParser

* add test for macro that accepts value

* More space in forms
Retain rules when converted to static
no duplicate names allowed

* Better error feedback
Update related devices on save

* Add button icon

* format

* update docs

* fix tests

* Fix some QueryBuilderFluentParser issues with OR
updated/more test data

* Show device groups runtime
fix querybuilder.json format

* Store table joins in the rules to minimize polling time
Update group joins in daily.sh (and when they are saved)

* Update daily.php

* Add units to time
This commit is contained in:
Tony Murray 2019-06-19 16:01:53 -05:00 committed by GitHub
parent d8931e1946
commit 1a60c44eb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1065 additions and 1360 deletions

View File

@ -160,7 +160,6 @@ class QueryBuilderFilter implements \JsonSerializable
return null;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php

View File

@ -0,0 +1,188 @@
<?php
/**
* QueryBuilderFluentParser.php
*
* -Description-
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2019 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Alerting;
use DB;
use Illuminate\Database\Query\Builder;
use Log;
class QueryBuilderFluentParser extends QueryBuilderParser
{
/**
* Convert the query builder rules to a Laravel Fluent builder
*
* @return Builder
*/
public function toQuery()
{
if (empty($this->builder) || !array_key_exists('condition', $this->builder)) {
return null;
}
$query = DB::table('devices');
$this->joinTables($query);
$this->parseGroupToQuery($query, $this->builder);
return $query;
}
/**
* @param Builder $query
* @param array $rule
* @param string $parent_condition AND or OR (for root, this should be null)
* @return Builder
*/
protected function parseGroupToQuery($query, $rule, $parent_condition = null)
{
return $query->where(function ($query) use ($rule, $parent_condition) {
foreach ($rule['rules'] as $group_rule) {
if (array_key_exists('condition', $group_rule)) {
$this->parseGroupToQuery($query, $group_rule, $rule['condition']);
} else {
$this->parseRuleToQuery($query, $group_rule, $rule['condition']);
}
}
}, null, null, $parent_condition ?? $rule['condition']);
}
/**
* @param Builder $query
* @param array $rule
* @param string $condition AND or OR
* @return Builder
*/
protected function parseRuleToQuery($query, $rule, $condition)
{
list($field, $op, $value) = $this->expandRule($rule);
switch ($op) {
case 'equal':
case 'not_equal':
case 'less':
case 'less_or_equal':
case 'greater':
case 'greater_or_equal':
case 'regex':
case 'not_regex':
return $query->where($field, self::$operators[$op], $value, $condition);
case 'contains':
case 'not_contains':
return $query->where($field, self::$operators[$op], "%$value%", $condition);
case 'begins_with':
case 'not_begins_with':
return $query->where($field, self::$operators[$op], "$value%", $condition);
case 'ends_with':
case 'not_ends_with':
return $query->where($field, self::$operators[$op], "%$value", $condition);
case 'is_empty':
case 'is_not_empty':
return $query->where($field, self::$operators[$op], '');
case 'is_null':
case 'is_not_null':
return $query->whereNull($field, $condition, $op == 'is_not_null');
case 'between':
case 'not_between':
return $query->whereBetween($field, $value, $condition, $op == 'not_between');
case 'in':
case 'not_in':
$values = preg_split('/[, ]/', $value);
if ($values !== false) {
return $query->whereIn($field, $values, $condition, $op == 'not_in');
}
Log::error('Could not parse in values, use comma or space delimiters');
break;
default:
Log::error('Unhandled QueryBuilderFluentParser operation: ' . $op);
}
return $query;
}
/**
* Extract field, operator and value from the rule and expand macros and raw values
*
* @param array $rule
* @return array [field, operator, value]
*/
protected function expandRule($rule)
{
$field = $rule['field'];
if (starts_with($field, 'macros.')) {
$field = DB::raw($this->expandMacro($field));
}
$op = $rule['operator'];
$value = $rule['value'];
if (!is_array($value) && starts_with($value, '`') && ends_with($value, '`')) {
$value = DB::raw($this->expandMacro(trim($value, '`')));
}
return [$field, $op, $value];
}
/**
* @param Builder $query
* @return Builder
*/
protected function joinTables($query)
{
if (empty($this->builder['joins'])) {
$this->generateJoins();
}
foreach ($this->builder['joins'] as $join) {
list($rightTable, $left, $right) = $join;
$query->leftJoin($rightTable, $left, $right);
}
return $query;
}
/**
* Generate the joins for this rule and store them in the rule.
* This is an expensive operation.
*
* @return $this
*/
public function generateJoins()
{
$joins = [];
foreach ($this->generateGlue() as $glue) {
list($left, $right) = explode(' = ', $glue, 2);
if (str_contains($right, '.')) { // last line is devices.device_id = ? for alerting... ignore it
list($rightTable, $rightKey) = explode('.', $right);
$joins[] = [$rightTable, $left, $right];
}
}
$this->builder['joins'] = $joins;
return $this;
}
}

View File

@ -30,7 +30,7 @@ use LibreNMS\DB\Schema;
class QueryBuilderParser implements \JsonSerializable
{
private static $legacy_operators = [
protected static $legacy_operators = [
'=' => 'equal',
'!=' => 'not_equal',
'~' => 'regex',
@ -40,7 +40,7 @@ class QueryBuilderParser implements \JsonSerializable
'<=' => 'less_or_equal',
'>=' => 'greater_or_equal',
];
private static $operators = [
protected static $operators = [
'equal' => "=",
'not_equal' => "!=",
'less' => "<",
@ -63,7 +63,7 @@ class QueryBuilderParser implements \JsonSerializable
'not_regex' => 'NOT REGEXP',
];
private static $values = [
protected static $values = [
'between' => "? AND ?",
'not_between' => "? AND ?",
'begins_with' => "'?%'",
@ -78,8 +78,8 @@ class QueryBuilderParser implements \JsonSerializable
'is_not_empty' => "''",
];
private $builder;
private $schema;
protected $builder;
protected $schema;
private function __construct(array $builder)
{
@ -107,7 +107,7 @@ class QueryBuilderParser implements \JsonSerializable
* @param array $rules
* @return array List of tables found in rules
*/
private function findTablesRecursive($rules)
protected function findTablesRecursive($rules)
{
$tables = [];
@ -167,13 +167,17 @@ class QueryBuilderParser implements \JsonSerializable
$split = array_chunk(preg_split('/(&&|\|\|)/', $query, -1, PREG_SPLIT_DELIM_CAPTURE), 2);
foreach ($split as $chunk) {
list($rule_text, $rule_operator) = $chunk;
if (count($chunk) < 2 && empty($chunk[0])) {
continue; // likely the ending && or ||
}
@list($rule_text, $rule_operator) = $chunk;
if (!isset($condition)) {
// only allow one condition. Since old rules had no grouping, this should hold logically
$condition = ($rule_operator == '||' ? 'OR' : 'AND');
}
list($field, $op, $value) = preg_split('/ *([!=<>~]{1,2}) */', trim($rule_text), 2, PREG_SPLIT_DELIM_CAPTURE);
@list($field, $op, $value) = preg_split('/ *([!=<>~]{1,2}) */', trim($rule_text), 2, PREG_SPLIT_DELIM_CAPTURE);
$field = ltrim($field, '%');
// for rules missing values just use '= 1'
@ -181,11 +185,12 @@ class QueryBuilderParser implements \JsonSerializable
if (is_null($value)) {
$value = '1';
} else {
$value = trim($value, '"');
// value is a field, mark it with backticks
if (starts_with($value, '%')) {
$value = '`' . ltrim($value, '%') . '`';
} else {
// but if it has quotes just remove the %
$value = ltrim(trim($value, '"'), '%');
}
// replace regex placeholder, don't think we can safely convert to like operators
@ -236,7 +241,7 @@ class QueryBuilderParser implements \JsonSerializable
if ($expand) {
$sql = 'SELECT * FROM ' .implode(',', $this->getTables());
$sql .= ' WHERE ' . $this->generateGlue() . ' AND ';
$sql .= ' WHERE (' . implode(' AND ', $this->generateGlue()) . ') AND ';
// only wrap in ( ) if the condition is OR and there is more than one rule
$wrap = $this->builder['condition'] == 'OR' && count($this->builder['rules']) > 1;
@ -281,7 +286,7 @@ class QueryBuilderParser implements \JsonSerializable
* @param bool $expand Expand macros?
* @return string
*/
private function parseRule($rule, $expand = false)
protected function parseRule($rule, $expand = false)
{
$field = $rule['field'];
$builder_op = $rule['operator'];
@ -320,7 +325,7 @@ class QueryBuilderParser implements \JsonSerializable
* @param int $depth_limit
* @return string|array
*/
private function expandMacro($subject, $tables_only = false, $depth_limit = 20)
protected function expandMacro($subject, $tables_only = false, $depth_limit = 20)
{
if (!str_contains($subject, 'macros.')) {
return $subject;
@ -361,9 +366,9 @@ class QueryBuilderParser implements \JsonSerializable
* Generate glue and first part of sql query for this rule
*
* @param string $target the name of the table to target, for alerting, this should be devices
* @return string
* @return array
*/
private function generateGlue($target = 'devices')
protected function generateGlue($target = 'devices')
{
$tables = $this->getTables(); // get all tables in query
@ -382,9 +387,7 @@ class QueryBuilderParser implements \JsonSerializable
}
// remove duplicates
$glue = array_unique($glue);
return '(' . implode(' AND ', $glue) . ')';
return array_unique($glue);
}
/**

View File

@ -214,7 +214,7 @@ class Schema
return [$table, $target];
}
$table_relations = $relationships[$table];
$table_relations = $relationships[$table] ?? [];
d_echo("Searching $table: " . json_encode($table_relations) . PHP_EOL);
if (!empty($table_relations)) {

View File

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers;
use App\Models\DeviceGroup;
use Illuminate\Validation\Rule;
use Illuminate\Http\Request;
use LibreNMS\Alerting\QueryBuilderFilter;
use LibreNMS\Alerting\QueryBuilderFluentParser;
use Toastr;
class DeviceGroupController extends Controller
{
public function __construct()
{
$this->authorizeResource(DeviceGroup::class, 'device_group');
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$this->authorize('manage', DeviceGroup::class);
return view('device-group.index', [
'device_groups' => DeviceGroup::orderBy('name')->withCount('devices')->get(),
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('device-group.create', [
'device_group' => new DeviceGroup(),
'filters' => json_encode(new QueryBuilderFilter('group')),
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|string|unique:device_groups',
'type' => 'required|in:dynamic,static',
'devices' => 'array|required_if:type,static',
'devices.*' => 'integer',
'rules' => 'json|required_if:type,dynamic',
]);
$deviceGroup = DeviceGroup::make($request->only(['name', 'desc', 'type']));
$deviceGroup->rules = json_decode($request->rules);
$deviceGroup->save();
if ($request->type == 'static') {
$deviceGroup->devices()->sync($request->devices);
}
Toastr::success(__('Device Group :name created', ['name' => $deviceGroup->name]));
return redirect()->route('device-groups.index');
}
/**
* Display the specified resource.
*
* @param \App\Models\DeviceGroup $deviceGroup
* @return \Illuminate\Http\Response
*/
public function show(DeviceGroup $deviceGroup)
{
return redirect(url('/devices/group=' . $deviceGroup->id));
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\DeviceGroup $deviceGroup
* @return \Illuminate\Http\Response
*/
public function edit(DeviceGroup $deviceGroup)
{
// convert old rules on edit
if (is_null($deviceGroup->rules)) {
$query_builder = QueryBuilderFluentParser::fromOld($deviceGroup->pattern);
$deviceGroup->rules = $query_builder->toArray();
}
return view('device-group.edit', [
'device_group' => $deviceGroup,
'filters' => json_encode(new QueryBuilderFilter('group')),
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\DeviceGroup $deviceGroup
* @return \Illuminate\Http\Response
*/
public function update(Request $request, DeviceGroup $deviceGroup)
{
$this->validate($request, [
'name' => [
'required',
'string',
Rule::unique('device_groups')->where(function ($query) use ($deviceGroup) {
$query->where('id', '!=', $deviceGroup->id);
}),
],
'type' => 'required|in:dynamic,static',
'devices' => 'array|required_if:type,static',
'devices.*' => 'integer',
'rules' => 'json|required_if:type,dynamic',
]);
$deviceGroup->fill($request->only(['name', 'desc', 'type']));
$devices_updated = false;
if ($deviceGroup->type == 'static') {
// sync device_ids from input
$devices_updated = array_sum($deviceGroup->devices()->sync($request->get('devices', [])));
} else {
$deviceGroup->rules = json_decode($request->rules);
}
if ($deviceGroup->isDirty() || $devices_updated) {
try {
if ($deviceGroup->save() || $devices_updated) {
Toastr::success(__('Device Group :name updated', ['name' => $deviceGroup->name]));
} else {
Toastr::error(__('Failed to save'));
return redirect()->back()->withInput();
}
} catch (\Illuminate\Database\QueryException $e) {
return redirect()->back()->withInput()->withErrors([
'rules' => __('Rules resulted in invalid query: ') . $e->getMessage()
]);
}
} else {
Toastr::info(__('No changes made'));
}
return redirect()->route('device-groups.index');
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\DeviceGroup $deviceGroup
* @return \Illuminate\Http\Response
*/
public function destroy(DeviceGroup $deviceGroup)
{
$deviceGroup->delete();
Toastr::success(__('Device Group :name deleted', ['name' => $deviceGroup->name]));
return redirect()->route('device-groups.index');
}
}

View File

@ -25,263 +25,101 @@
namespace App\Models;
use LibreNMS\Alerting\QueryBuilderFluentParser;
use Log;
use Permissions;
use DB;
class DeviceGroup extends BaseModel
{
public $timestamps = false;
protected $appends = ['patternSql'];
protected $fillable = ['name', 'desc', 'pattern', 'params'];
protected $casts = ['params' => 'array'];
protected $fillable = ['name', 'desc', 'type'];
protected $casts = ['rules' => 'array'];
public static function boot()
{
parent::boot();
static::deleting(function (DeviceGroup $deviceGroup) {
$deviceGroup->devices()->detach();
});
static::saving(function (DeviceGroup $deviceGroup) {
if ($deviceGroup->isDirty('rules')) {
$deviceGroup->rules = $deviceGroup->getParser()->generateJoins()->toArray();
$deviceGroup->updateDevices();
}
});
}
// ---- Helper Functions ----
public function updateRelations()
/**
* Update devices included in this group (dynamic only)
*/
public function updateDevices()
{
// we need an id to add relationships
if (is_null($this->id)) {
$this->save();
if ($this->type == 'dynamic') {
$this->devices()->sync(QueryBuilderFluentParser::fromJSON($this->rules)->toQuery()
->distinct()->pluck('devices.device_id'));
}
$device_ids = $this->getDeviceIdsRaw();
// update the relationships (deletes and adds as needed)
$this->devices()->sync($device_ids);
}
/**
* Get an array of the device ids from this group by re-querying the database with
* either the specified pattern or the saved pattern of this group
* Update the device groups for the given device or device_id
*
* @param string $statement Optional, will use the pattern from this group if not specified
* @param array $params array of paremeters
* @param Device|int $device
* @return array
*/
public function getDeviceIdsRaw($statement = null, $params = null)
public static function updateGroupsFor($device)
{
if (is_null($statement)) {
$statement = $this->pattern;
$device = ($device instanceof Device ? $device : Device::find($device));
if (!$device instanceof Device) {
// could not load device
return [
"attached" => [],
"detached" => [],
"updated" => [],
];
}
if (is_null($params)) {
if (empty($this->params)) {
if (!starts_with($statement, '%')) {
// can't build sql
return [];
}
} else {
$params = $this->params;
}
}
$statement = $this->applyGroupMacros($statement);
$tables = $this->getTablesFromPattern($statement);
$query = null;
if (count($tables) == 1) {
$query = DB::table($tables[0])->select('device_id')->distinct();
} else {
$query = DB::table('devices')->select('devices.device_id')->distinct();
foreach ($tables as $table) {
// skip devices table, we used that as the base.
if ($table == 'devices') {
continue;
$device_group_ids = static::query()
->with(['devices' => function ($query) {
$query->select('devices.device_id');
}])
->get()
->filter(function ($device_group) use ($device) {
/** @var DeviceGroup $device_group */
if ($device_group->type == 'dynamic') {
try {
return $device_group->getParser()
->toQuery()
->where('devices.device_id', $device->device_id)
->exists();
} catch (\Illuminate\Database\QueryException $e) {
Log::error("Device Group '$device_group->name' generates invalid query: " . $e->getMessage());
return false;
}
}
$query = $query->join($table, 'devices.device_id', '=', $table.'.device_id');
}
}
// for static, if this device is include, keep it.
return $device_group->devices
->where('device_id', $device->device_id)
->isNotEmpty();
})->pluck('id');
// match the device ids
if (is_null($params)) {
return $query->whereRaw($statement)->pluck('device_id')->toArray();
} else {
return $query->whereRaw($statement, $params)->pluck('device_id')->toArray();
}
return $device->groups()->sync($device_group_ids);
}
/**
* Process Macros
* Get a query builder parser instance from this device group
*
* @param string $pattern Rule to process
* @param int $x Recursion-Anchor, do not pass
* @return string|boolean
* @return QueryBuilderFluentParser
*/
public static function applyGroupMacros($pattern, $x = 1)
public function getParser()
{
if (!str_contains($pattern, 'macros.')) {
return $pattern;
}
foreach (\LibreNMS\Config::get('alert.macros.group', []) as $macro => $value) {
$value = str_replace(['%', '&&', '||'], ['', 'AND', 'OR'], $value); // this might need something more complex
if (!str_contains($macro, ' ')) {
$pattern = str_replace('macros.'.$macro, '('.$value.')', $pattern);
}
}
if (str_contains($pattern, 'macros.')) {
if (++$x < 30) {
$pattern = self::applyGroupMacros($pattern, $x);
} else {
return false;
}
}
return $pattern;
}
/**
* Extract an array of tables in a pattern
*
* @param string $pattern
* @return array
*/
private function getTablesFromPattern($pattern)
{
preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
if (is_null($tables)) {
return [];
}
return array_keys(array_flip($tables[0])); // unique tables only
}
/**
* Convert a v1 device group pattern to sql that can be ingested by jQuery-QueryBuilder
*
* @param $pattern
* @return array
*/
private function convertV1Pattern($pattern)
{
$pattern = rtrim($pattern, ' &&');
$pattern = rtrim($pattern, ' ||');
$ops = ['=', '!=', '<', '<=', '>', '>='];
$parts = str_getcsv($pattern, ' '); // tokenize the pattern, respecting quoted parts
$out = "";
$count = count($parts);
for ($i = 0; $i < $count; $i++) {
$cur = $parts[$i];
if (starts_with($cur, '%')) {
// table and column or macro
$out .= substr($cur, 1).' ';
} elseif (substr($cur, -1) == '~') {
// like operator
$content = $parts[++$i]; // grab the content so we can format it
if (starts_with($cur, '!')) {
// prepend NOT
$out .= 'NOT ';
}
$out .= "LIKE('".$this->convertRegexToLike($content)."') ";
} elseif ($cur == '&&') {
$out .= 'AND ';
} elseif ($cur == '||') {
$out .= 'OR ';
} elseif (in_array($cur, $ops)) {
// pass-through operators
$out .= $cur.' ';
} else {
// user supplied input
$out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input
}
}
return rtrim($out);
}
/**
* Convert sql regex to like, many common uses can be converted
* Should only be used to convert v1 patterns
*
* @param $pattern
* @return string
*/
private function convertRegexToLike($pattern)
{
$startAnchor = starts_with($pattern, '^');
$endAnchor = ends_with($pattern, '$');
$pattern = trim($pattern, '^$');
$wildcards = ['@', '.*'];
if (str_contains($pattern, $wildcards)) {
// contains wildcard
$pattern = str_replace($wildcards, '%', $pattern);
}
// add ends appropriately
if ($startAnchor && !$endAnchor) {
$pattern .= '%';
} elseif (!$startAnchor && $endAnchor) {
$pattern = '%'.$pattern;
}
// if there are no wildcards, assume substring
if (!str_contains($pattern, '%')) {
$pattern = '%'.$pattern.'%';
}
return $pattern;
}
// ---- Accessors/Mutators ----
/**
* Returns an sql formatted string
* Mostly, this is for ingestion by JQuery-QueryBuilder
*
* @return string
*/
public function getPatternSqlAttribute()
{
$sql = $this->pattern;
// fill in parameters
foreach ((array)$this->params as $value) {
if (!is_numeric($value) && !starts_with($value, "'")) {
$value = "'".$value."'";
}
$sql = preg_replace('/\?/', $value, $sql, 1);
}
return $sql;
}
/**
* Custom mutator for params attribute
* Allows already encoded json to pass through
*
* @param array|string $params
*/
// public function setParamsAttribute($params)
// {
// if (!Util::isJson($params)) {
// $params = json_encode($params);
// }
//
// $this->attributes['params'] = $params;
// }
/**
* Check if the stored pattern is v1
* Convert it to v2 for display
* Currently, it will only be updated in the database if the user saves the rule in the ui
*
* @param $pattern
* @return string
*/
public function getPatternAttribute($pattern)
{
// If this is a v1 pattern, convert it to sql
if (starts_with($pattern, '%')) {
return $this->convertV1Pattern($pattern);
}
return $pattern;
return !empty($this->rules) ?
QueryBuilderFluentParser::fromJson($this->rules) :
QueryBuilderFluentParser::fromOld($this->pattern);
}
// ---- Query Scopes ----
@ -297,17 +135,6 @@ class DeviceGroup extends BaseModel
// ---- Define Relationships ----
public function alertSchedules()
{
return $this->morphToMany('App\Models\AlertSchedule', 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'schedule_id');
}
public function rules()
{
return $this->belongsToMany('App\Models\AlertRule', 'alert_group_map', 'group_id', 'rule_id');
}
public function devices()
{
return $this->belongsToMany('App\Models\Device', 'device_group_device', 'device_group_id', 'device_id');

View File

@ -0,0 +1,101 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\DeviceGroup;
use Illuminate\Auth\Access\HandlesAuthorization;
class DeviceGroupPolicy
{
use HandlesAuthorization;
public function before($user, $ability)
{
if ($user->isAdmin()) {
return true;
}
}
/**
* Determine whether the user can manage device groups.
*
* @param \App\Models\User $user
* @return bool
*/
public function manage(User $user)
{
return false;
}
/**
* Determine whether the user can view the device group.
*
* @param \App\Models\User $user
* @param \App\Models\DeviceGroup $deviceGroup
* @return mixed
*/
public function view(User $user, DeviceGroup $deviceGroup)
{
return false;
}
/**
* Determine whether the user can create device groups.
*
* @param \App\Models\User $user
* @return mixed
*/
public function create(User $user)
{
return false;
}
/**
* Determine whether the user can update the device group.
*
* @param \App\Models\User $user
* @param \App\Models\DeviceGroup $deviceGroup
* @return mixed
*/
public function update(User $user, DeviceGroup $deviceGroup)
{
return false;
}
/**
* Determine whether the user can delete the device group.
*
* @param \App\Models\User $user
* @param \App\Models\DeviceGroup $deviceGroup
* @return mixed
*/
public function delete(User $user, DeviceGroup $deviceGroup)
{
return false;
}
/**
* Determine whether the user can restore the device group.
*
* @param \App\Models\User $user
* @param \App\Models\DeviceGroup $deviceGroup
* @return mixed
*/
public function restore(User $user, DeviceGroup $deviceGroup)
{
return false;
}
/**
* Determine whether the user can permanently delete the device group.
*
* @param \App\Models\User $user
* @param \App\Models\DeviceGroup $deviceGroup
* @return mixed
*/
public function forceDelete(User $user, DeviceGroup $deviceGroup)
{
return false;
}
}

View File

@ -2,7 +2,9 @@
namespace App\Providers;
use App\Models\DeviceGroup;
use App\Models\User;
use App\Policies\DeviceGroupPolicy;
use App\Policies\UserPolicy;
use App\Guards\ApiTokenGuard;
use Auth;
@ -17,7 +19,8 @@ class AuthServiceProvider extends ServiceProvider
* @var array
*/
protected $policies = [
User::class => UserPolicy::class
User::class => UserPolicy::class,
DeviceGroup::class => DeviceGroupPolicy::class,
];
/**

View File

@ -7,6 +7,7 @@
*/
use App\Models\Device;
use App\Models\DeviceGroup;
use Illuminate\Database\Eloquent\Collection;
use LibreNMS\Config;
use LibreNMS\Exceptions\LockException;
@ -287,6 +288,26 @@ if ($options['f'] === 'refresh_alert_rules') {
}
}
if ($options['f'] === 'refresh_device_groups') {
try {
if (Config::get('distributed_poller')) {
MemcacheLock::lock('refresh_device_groups', 0, 86000);
}
echo 'Refreshing device group table relationships' . PHP_EOL;
DeviceGroup::all()->each(function ($deviceGroup) {
if ($deviceGroup->type == 'dynamic') {
/** @var DeviceGroup $deviceGroup */
$deviceGroup->rules = $deviceGroup->getParser()->generateJoins()->toArray();
$deviceGroup->save();
}
});
} catch (LockException $e) {
echo $e->getMessage() . PHP_EOL;
exit(-1);
}
}
if ($options['f'] === 'notify') {
if (isset($config['alert']['default_mail'])) {
send_mail(

View File

@ -262,6 +262,7 @@ main () {
# Cleanups
local options=("refresh_alert_rules"
"refresh_os_cache"
"refresh_device_groups"
"recalculate_device_dependencies"
"syslog"
"eventlog"

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class DeviceGroupsRewrite extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('device_groups', function (Blueprint $table) {
$table->string('desc')->nullable()->change();
$table->string('type', 16)->default('dynamic')->after('desc');
$table->text('rules')->nullable()->after('type');
$table->dropColumn('params');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('device_groups', function (Blueprint $table) {
$table->string('desc')->change();
$table->dropColumn('type');
$table->dropColumn('rules');
$table->text('params')->nullable()->after('pattern');
});
}
}

View File

@ -3,33 +3,24 @@ path: blob/master/doc/
LibreNMS supports grouping your devices together in much the same way as you can configure alerts. This document will hopefully help you get started.
### Patterns
# Dynamic Groups
Patterns work in the same way as Entities within the alerting system, the format of the pattern is based on the MySQL structure your data is in. Such
as __tablename.columnname__. If you already know the entity you want, you can browse around inside MySQL using `show tables` and `desc <tablename>`.
### Rule Editor
As a working example and a common question, let's assume you want to group devices by hostname. If your hostname format is dcX.[devicetype].example.com. You would use the pattern
`devices.hostname`. Select the condition which in this case would be `Like` and then enter `dc1\..*\.example.com`. This would then match dc1.sw01.example.com, dc1.rtr01.example.com but not
dc2.sw01.example.com.
The rule is based on the MySQL structure your data is in. Such as __tablename.columnname__.
If you already know the entity you want, you can browse around inside MySQL using `show tables` and `desc <tablename>`.
#### Wildcards
As a working example and a common question, let's assume you want to group devices by hostname. If your hostname format is dcX.[devicetype].example.com. You would use the field
`devices.hostname`.
As with alerts, the `Like` operation allows MySQL Regular expressions.
If you want to group them by device type, you would add a rule for routers of `devices.hostname` endswith `rtr.example.com`.
A list of common entities is maintained in our [Alerting docs](/Alerting/Entities/).
If you want to group them by DC, you could use the rule `devices.hostname` regex `dc1\..*\.example\.com` (Don't forget to escape periods in the regex)
### Conditions
# Static Groups
Please see our [Alerting docs](/Alerting/Rules/#syntax) for explanations.
### Connection
If you only want to group based on one pattern then select And. If however you want to build a group based on multiple patterns then you can build a SQL like
query using And / Or.
As an example, we want to base our group on the devices hostname AND it's type. Use the pattern as before, `devices.hostname`, select
the condition which in this case would be `Like` and then enter `dc1.@.example.com` then click And. Now enter `devices.type` in the pattern, select `Equals`
and enter `firewall`. This would then match dc1.fw01.example.com but not dc1.sw01.example.com as that is a network type.
You can create static groups (and convert dynamic groups to static) to put specific devices in a group.
Just select static as the type and select the devices you want in the group.
![Device Groups](/img/device_groups.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,138 +0,0 @@
<?php
/*
* Copyright (C) 2014 Daniel Preussker <f0o@devilcode.org>
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Rule Suggestion-AJAX
* @author Daniel Preussker <f0o@devilcode.org>
* @copyright 2014 f0o, LibreNMS
* @license GPL
* @package LibreNMS/Alerts
*/
use LibreNMS\Authentication\LegacyAuth;
$init_modules = array('web', 'auth');
require realpath(__DIR__ . '/..') . '/includes/init.php';
if (!LegacyAuth::check()) {
die('Unauthorized');
}
set_debug($_REQUEST['debug']);
/**
* Levenshtein Sort
* @param string $base Comparison basis
* @param array $obj Object to sort
* @return array
*/
function levsort($base, $obj)
{
$ret = array();
foreach ($obj as $elem) {
$lev = levenshtein($base, $elem, 1, 10, 10);
if ($lev == 0) {
return array(array('name' => $elem));
} else {
while (isset($ret["$lev"])) {
$lev += 0.1;
}
$ret["$lev"] = array('name' => $elem);
}
}
ksort($ret);
return $ret;
}
header('Content-type: application/json');
$obj = array(array('name' => 'Error: No suggestions found.'));
$term = array();
$current = false;
if (isset($_GET['term'], $_GET['device_id'])) {
$chk = array();
$_GET['term'] = mres($_GET['term']);
$_GET['device_id'] = mres($_GET['device_id']);
if (strstr($_GET['term'], '.')) {
$term = explode('.', $_GET['term']);
if ($term[0] == 'macros') {
foreach ($config['alert']['macros']['rule'] as $macro => $v) {
$chk[] = 'macros.'.$macro;
}
} else {
$tmp = dbFetchRows('SHOW COLUMNS FROM '.$term[0]);
foreach ($tmp as $tst) {
if (isset($tst['Field'])) {
$chk[] = $term[0].'.'.$tst['Field'];
}
}
}
$current = true;
} else {
$tmp = dbFetchRows("SELECT TABLE_NAME FROM information_schema.COLUMNS WHERE COLUMN_NAME = 'device_id'");
foreach ($tmp as $tst) {
$chk[] = $tst['TABLE_NAME'].'.';
}
$chk[] = 'macros.';
$chk[] = 'bills.';
}
if (sizeof($chk) > 0) {
$obj = levsort($_GET['term'], $chk);
$obj = array_chunk($obj, 20, true);
$obj = $obj[0];
$flds = array();
if ($current === true) {
foreach ($obj as $fld) {
$flds[] = $fld['name'];
}
$qry = dbFetchRows('SELECT '.implode(', ', $flds).' FROM '.$term[0].' WHERE device_id = ?', array($_GET['device_id']));
$ret = array();
foreach ($obj as $lev => $fld) {
list($tbl, $chk) = explode('.', $fld['name']);
$val = array();
foreach ($qry as $row) {
$val[] = $row[$chk];
}
$ret[$lev] = array(
'name' => $fld['name'],
'current' => $val,
);
}
$obj = $ret;
}
}
} elseif ($vars['type'] === 'alert_rule_collection') {
$x=0;
foreach (get_rules_from_json() as $rule) {
if (str_i_contains($rule['name'], $vars['term'])) {
$rule['id'] = $x;
$tmp[] = $rule;
}
$x++;
}
if (is_array($tmp)) {
$obj = $tmp;
}
}
die(json_encode($obj));

View File

@ -1833,21 +1833,3 @@ function array_by_column($array, $column)
{
return array_combine(array_column($array, $column), $array);
}
/**
* Get all consecutive pairs of values in an array.
* [1,2,3,4] -> [[1,2],[2,3],[3,4]]
*
* @param array $array
* @return array
*/
function array_pairs($array)
{
$pairs = [];
for ($i = 1; $i < count($array); $i++) {
$pairs[] = [$array[$i -1], $array[$i]];
}
return $pairs;
}

View File

@ -959,9 +959,6 @@ $config['leaflet']['tile_url'] = "{s}.tile.openstreetma
// General GUI options
$config['gui']['network-map']['style'] = 'new';//old is also valid
// Navbar variables
$config['navbar']['manage_groups']['hide'] = 0;
// Show errored ports in the summary table on the dashboard
$config['summary_errors'] = 0;

View File

@ -1,371 +0,0 @@
<?php
/*
* Copyright (C) 2015 Daniel Preussker <f0o@devilcode.org>
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Device-Grouping
* @author Daniel Preussker <f0o@devilcode.org>
* @author Tony Murray <murrayotony@gmail.com>
* @copyright 2016 f0o, murrant, LibreNMS
* @license GPL
* @package LibreNMS
* @subpackage Devices
*/
use LibreNMS\Config;
/**
* Add a new device group
* @param $pattern
* @param $name
* @param $desc
* @return int|string
*/
function AddDeviceGroup($name, $desc, $pattern)
{
$group_id = dbInsert(array('name' => $name, 'desc' => $desc, 'pattern' => $pattern), 'device_groups');
if ($group_id) {
UpdateDeviceGroup($group_id);
}
return $group_id;
}
/**
* Update a device group
* @param $group_id
* @param $pattern
* @param $name
* @param $desc
* @return bool
*/
function EditDeviceGroup($group_id, $name = null, $desc = null, $pattern = null)
{
$vars = array();
if (!is_null($name)) {
$vars['name'] = $name;
}
if (!is_null($desc)) {
$vars['desc'] = $desc;
}
if (!is_null($pattern)) {
$vars['pattern'] = $pattern;
}
$success = dbUpdate($vars, 'device_groups', 'id=?', array($group_id)) >= 0;
if ($success) {
UpdateDeviceGroup($group_id);
}
return $success;
}
/**
* Generate SQL from Group-Pattern
* @param string $pattern Pattern to generate SQL for
* @param string $search What to searchid for
* @param int $extra
* @return string sql to perform the search
*/
function GenGroupSQL($pattern, $search = '', $extra = 0)
{
$pattern = RunGroupMacros($pattern);
if ($pattern === false) {
return false;
}
if (starts_with($pattern, '%')) {
// v1 pattern
$tables = array();
$words = explode(' ', $pattern);
foreach ($words as $word) {
if (starts_with($word, '%') && str_contains($word, '.')) {
list($table, $column) = explode('.', $word, 2);
$table = str_replace('%', '', $table);
$tables[] = mres(str_replace('(', '', $table));
$pattern = str_replace($word, $table . '.' . $column, $pattern);
}
}
$tables = array_keys(array_flip($tables));
} else {
// v2 pattern
$tables = getTablesFromPattern($pattern);
}
$pattern = rtrim($pattern, '&|');
if ($tables[0] != 'devices' && dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', array($tables[0],'device_id')) != 1) {
//Our first table has no valid glue, prepend the 'devices' table to it!
array_unshift($tables, 'devices');
$tables = array_unique($tables); // remove devices from later in the array if it exists
}
$x = sizeof($tables)-1;
$i = 0;
$join = "";
while ($i < $x) {
if (isset($tables[$i+1])) {
$gtmp = ResolveGlues(array($tables[$i+1]), 'device_id');
if ($gtmp === false) {
//Cannot resolve glue-chain. Rule is invalid.
return false;
}
$last = "";
$qry = "";
foreach ($gtmp as $glue) {
if (empty($last)) {
list($tmp,$last) = explode('.', $glue);
$qry .= $glue.' = ';
} else {
$parts = explode('.', $glue);
if (count($parts) == 3) {
list($tmp, $new, $last) = $parts;
} else {
list($tmp,$new) = $parts;
}
$qry .= $tmp.'.'.$last.' && '.$tmp.'.'.$new.' = ';
$last = $new;
}
if (!in_array($tmp, $tables)) {
$tables[] = $tmp;
}
}
$join .= "( ".$qry.$tables[0].".device_id ) && ";
}
$i++;
}
if ($extra === 1) {
$sql_extra = ",`devices`.*";
}
if (!empty($search)) {
$search = str_replace("(", "", $tables[0]).'.'.$search. ' AND';
}
if (!empty($join)) {
$join = '('.rtrim($join, '& ').') &&';
}
$sql = 'SELECT DISTINCT('.str_replace('(', '', $tables[0]).'.device_id)'.$sql_extra.' FROM '.implode(',', $tables).' WHERE '.$join.' '.$search.' ('.str_replace(array('%', '@', '!~', '~'), array('', '.*', 'NOT REGEXP', 'REGEXP'), $pattern).')';
return $sql;
}//end GenGroupSQL()
/**
* Extract an array of tables in a pattern
*
* @param string $pattern
* @return array
*/
function getTablesFromPattern($pattern)
{
preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables);
if (is_null($tables)) {
return array();
}
return array_keys(array_flip($tables[0])); // unique tables only
}
/**
* Run the group queries again to get fresh list of devices for this group
* @param integer $group_id Group-ID
* @return string
*/
function QueryDevicesFromGroup($group_id)
{
$group = dbFetchRow('SELECT pattern,params FROM device_groups WHERE id = ?', array($group_id));
$pattern = rtrim($group['pattern'], '&|');
$params = (array)json_decode($group['params']);
if (!empty($pattern)) {
$result = dbFetchColumn(GenGroupSQL($pattern), $params);
return $result;
}
return false;
}//end QueryDevicesFromGroup()
/**
* Get an array of all the device ids belonging to this group_id
* @param $group_id
* @param bool $nested Return an array of arrays containing 'device_id'. (for API compatibility)
* @param string $full Return all fields from devices_id
* @return array
*/
function GetDevicesFromGroup($group_id, $nested = false, $full = '')
{
if ($full) {
$query = 'SELECT `device_groups`.`name`, `devices`.* FROM `device_groups` INNER JOIN `device_group_device` ON `device_groups`.`id` = `device_group_device`.`device_group_id` INNER JOIN `devices` ON `device_group_device`.`device_id` = `devices`.`device_id` WHERE `device_groups`.`id` = ?';
} else {
$query = 'SELECT `device_id` FROM `device_group_device` WHERE `device_group_id` = ? ';
}
if ($nested) {
return dbFetchRows($query, array($group_id));
} else {
return dbFetchColumn($query, array($group_id));
}
}//end GetDevicesFromGroup()
/**
* Get all Device-Groups
* @return array
*/
function GetDeviceGroups()
{
return dbFetchRows('SELECT * FROM device_groups ORDER BY name');
}//end GetDeviceGroups()
/**
* Run the group queries again to get fresh list of groups for this device
* @param integer $device_id Device-ID
* @param int $extra Return extra info about the groups (name, desc, pattern)
* @return array
*/
function QueryGroupsFromDevice($device_id, $extra = 0)
{
$ret = array();
foreach (GetDeviceGroups() as $group) {
$params = (array)json_decode($group['params']);
array_unshift($params, $device_id);
if (dbFetchCell(GenGroupSQL($group['pattern'], 'device_id=?', $extra).' LIMIT 1', $params) == $device_id) {
if ($extra === 0) {
$ret[] = $group['id'];
} else {
$ret[] = $group;
}
}
}
return $ret;
}//end QueryGroupsFromDevice()
/**
* Get the Device Group IDs of a Device from the database
* @param $device_id
* @param int $extra Return extra info about the groups (name, desc, pattern)
* @return array
*/
function GetGroupsFromDevice($device_id, $extra = 0)
{
$ret = array();
if ($extra === 0) {
$ret = dbFetchColumn('SELECT `device_group_id` FROM `device_group_device` WHERE `device_id`=?', array($device_id));
} else {
$ret = dbFetchRows('SELECT `device_groups`.* FROM `device_group_device` LEFT JOIN `device_groups` ON `device_group_device`.`device_group_id`=`device_groups`.`id` WHERE `device_group_device`.`device_id`=?', array($device_id));
}
return $ret;
}//end GetGroupsFromDeviceDB()
/**
* Process Macros
* @param string $rule Rule to process
* @param int $x Recursion-Anchor
* @return string|boolean
*/
function RunGroupMacros($rule, $x = 1)
{
$macros = Config::get('alert.macros.group', []);
krsort($macros);
foreach ($macros as $macro => $value) {
if (!strstr($macro, " ")) {
$rule = str_replace('%macros.'.$macro, '('.$value.')', $rule);
}
}
if (strstr($rule, "%macros")) {
if (++$x < 30) {
$rule = RunGroupMacros($rule, $x);
} else {
return false;
}
}
return $rule;
}//end RunGroupMacros()
/**
* Update device-group relationship for the given device id
* @param $device_id
*/
function UpdateGroupsForDevice($device_id)
{
d_echo("### Start Device Groups ###\n");
$queried_groups = QueryGroupsFromDevice($device_id);
$db_groups = GetGroupsFromDevice($device_id);
// compare the arrays to get the added and removed groups
$added_groups = array_diff($queried_groups, $db_groups);
$removed_groups = array_diff($db_groups, $queried_groups);
d_echo("Groups Added: ".implode(',', $added_groups).PHP_EOL);
d_echo("Groups Removed: ".implode(',', $removed_groups).PHP_EOL);
// insert new groups
$insert = array();
foreach ($added_groups as $group_id) {
$insert[] = array('device_id' => $device_id, 'device_group_id' => $group_id);
}
if (!empty($insert)) {
dbBulkInsert($insert, 'device_group_device');
}
// remove old groups
if (!empty($removed_groups)) {
dbDelete('device_group_device', '`device_id`=? AND `device_group_id` IN ' . dbGenPlaceholders(count($removed_groups)), array_merge([$device_id], $removed_groups));
}
d_echo("### End Device Groups ###\n");
}
/**
* Update the device-group relationship for the given group id
* @param $group_id
*/
function UpdateDeviceGroup($group_id)
{
$queried_devices = QueryDevicesFromGroup($group_id);
$db_devices = GetDevicesFromGroup($group_id);
// compare the arrays to get the added and removed devices
$added_devices = array_diff($queried_devices, $db_devices);
$removed_devices = array_diff($db_devices, $queried_devices);
// insert new devices
$insert = array();
foreach ($added_devices as $device_id) {
$insert[] = array('device_id' => $device_id, 'device_group_id' => $group_id);
}
if (!empty($insert)) {
dbBulkInsert($insert, 'device_group_device');
}
// remove old devices
if (!empty($removed_devices)) {
dbDelete('device_group_device', '`device_group_id`=? AND `device_id` IN ' . dbGenPlaceholders(count($removed_devices)), array_merge([$group_id], $removed_devices));
}
}
/**
* Fill in params into the pattern, replacing placeholders (?)
* If $params is empty or null, just returns $pattern
*
* @return string
*/
function formatDeviceGroupPattern($pattern, $params)
{
// fill in parameters
foreach ((array)$params as $value) {
if (!is_numeric($value) && !starts_with($value, "'")) {
$value = "'".$value."'";
}
$pattern = preg_replace('/\?/', $value, $pattern, 1);
}
return $pattern;
}

View File

@ -86,3 +86,23 @@ if (!function_exists('set_debug')) {
return $debug;
}
}
if (!function_exists('array_pairs')) {
/**
* Get all consecutive pairs of values in an array.
* [1,2,3,4] -> [[1,2],[2,3],[3,4]]
*
* @param array $array
* @return array
*/
function array_pairs($array)
{
$pairs = [];
for ($i = 1; $i < count($array); $i++) {
$pairs[] = [$array[$i - 1], $array[$i]];
}
return $pairs;
}
}

View File

@ -12,6 +12,8 @@
* the source code distribution for details.
*/
use App\Models\Device;
use App\Models\DeviceGroup;
use LibreNMS\Alerting\QueryBuilderParser;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
@ -826,7 +828,7 @@ function list_available_health_graphs()
'name' => 'device_'.$graph['sensor_class'],
);
}
$device = \App\Models\Device::find($device_id);
$device = Device::find($device_id);
if ($device) {
if ($device->processors()->count() > 0) {
@ -1820,21 +1822,24 @@ function get_device_groups()
{
$app = \Slim\Slim::getInstance();
$router = $app->router()->getCurrentRoute()->getParams();
$status = 'error';
$code = 404;
$hostname = $router['hostname'];
// use hostname as device_id if it's all digits
$device_id = ctype_digit($hostname) ? $hostname : getidbyname($hostname);
if (is_numeric($device_id)) {
$groups = GetGroupsFromDevice($device_id, 1);
if (!empty($router['hostname'])) {
$device = ctype_digit($router['hostname']) ? Device::find($router['hostname']) : Device::findByHostname($router['hostname']);
if (is_null($device)) {
api_error(404, 'Device not found');
}
$query = $device->groups();
} else {
$groups = GetDeviceGroups();
$query = DeviceGroup::query();
}
if (empty($groups)) {
$groups = $query->orderBy('name')->get();
if ($groups->isEmpty()) {
api_error(404, 'No device groups found');
}
api_success($groups, 'groups', 'Found ' . count($groups) . ' device groups');
api_success($groups->makeHidden('pivot')->toArray(), 'groups', 'Found ' . $groups->count() . ' device groups');
}
function get_devices_by_group()
@ -1842,19 +1847,25 @@ function get_devices_by_group()
check_is_read();
$app = \Slim\Slim::getInstance();
$router = $app->router()->getCurrentRoute()->getParams();
$name = urldecode($router['name']);
$devices = array();
$full = $_GET['full'];
if (empty($name)) {
if (empty($router['name'])) {
api_error(400, 'No device group name provided');
}
$group_id = dbFetchCell("SELECT `id` FROM `device_groups` WHERE `name`=?", array($name));
$devices = GetDevicesFromGroup($group_id, true, $full);
if (empty($devices)) {
$name = urldecode($router['name']);
$device_group = ctype_digit($name) ? DeviceGroup::find($name) : DeviceGroup::where('name', $name)->first();
if (empty($device_group)) {
api_error(404, 'Device group not found');
}
$devices = $device_group->devices()->get(empty($_GET['full']) ? ['devices.device_id'] : ['*']);
if ($devices->isEmpty()) {
api_error(404, 'No devices found in group ' . $name);
}
api_success($devices, 'devices');
api_success($devices->makeHidden('pivot')->toArray(), 'devices');
}
@ -2050,7 +2061,7 @@ function get_fdb()
}
check_device_permission($device_id);
$device = \App\Models\Device::find($device_id);
$device = Device::find($device_id);
if ($device) {
$fdb = $device->portsFdb;
api_success($fdb, 'ports_fdb');

View File

@ -115,11 +115,8 @@ if (defined('SHOW_SETTINGS')) {
<select class="form-control" name="group">';
$common_output[] = '<option value=""' . ($current_group == '' ? ' selected' : '') . '>any group</option>';
$device_groups = GetDeviceGroups();
$common_output[] = "<!-- " . print_r($device_groups, true) . " -->";
foreach ($device_groups as $group) {
$group_id = $group['id'];
$common_output[] = "<option value=\"$group_id\"" . (is_numeric($current_group) && $current_group == $group_id ? ' selected' : '') . ">" . $group['name'] . " - " . $group['description'] . "</option>";
foreach (\App\Models\DeviceGroup::orderBy('name')->get(['id', 'name', 'desc']) as $group) {
$common_output[] = "<option value=\"$group->id\"" . (is_numeric($current_group) && $current_group == $group->id ? ' selected' : '') . ">" . $group->name . " - " . $group->desc . "</option>";
}
$common_output[] = '
</select>

View File

@ -1,53 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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.
*/
use LibreNMS\Authentication\LegacyAuth;
if (!LegacyAuth::user()->hasGlobalAdmin()) {
die('ERROR: You need to be admin');
}
$pattern = $_POST['patterns'];
$group_id = $_POST['group_id'];
$name = mres($_POST['name']);
$desc = mres($_POST['desc']);
if (is_array($pattern)) {
$pattern = implode(' ', $pattern);
} elseif (!empty($_POST['pattern']) && !empty($_POST['condition']) && !empty($_POST['value'])) {
$pattern = '%'.$_POST['pattern'].' '.$_POST['condition'].' ';
if (is_numeric($_POST['value'])) {
$pattern .= $_POST['value'];
} else {
$pattern .= '"'.$_POST['value'].'"';
}
}
if (empty($pattern)) {
$update_message = 'ERROR: No group was generated';
} elseif (is_numeric($group_id) && $group_id > 0) {
if (EditDeviceGroup($group_id, $name, $desc, $pattern)) {
$update_message = "Edited Group: <i>$name: $pattern</i>";
} else {
$update_message = 'ERROR: Failed to edit Group: <i>'.$pattern.'</i>';
}
} else {
if (AddDeviceGroup($name, $desc, $pattern)) {
$update_message = "Added Group: <i>$name: $pattern</i>";
} else {
$update_message = 'ERROR: Failed to add Group: <i>'.$pattern.'</i>';
}
}
echo $update_message;

View File

@ -1,36 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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.
*/
use LibreNMS\Authentication\LegacyAuth;
header('Content-type: text/plain');
if (!LegacyAuth::user()->hasGlobalAdmin()) {
die('ERROR: You need to be admin');
}
if (!is_numeric($_POST['group_id'])) {
echo 'ERROR: No group selected';
exit;
} else {
if (dbDelete('device_groups', '`id` = ?', array($_POST['group_id']))) {
dbDelete('alert_group_map', 'group_id=?', [$_POST['group_id']]);
echo 'Group has been deleted.';
exit;
} else {
echo 'ERROR: Group has not been deleted.';
exit;
}
}

View File

@ -1,41 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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.
*/
use LibreNMS\Authentication\LegacyAuth;
if (!LegacyAuth::user()->hasGlobalAdmin()) {
header('Content-type: text/plain');
die('ERROR: You need to be admin');
}
$group_id = $_POST['group_id'];
if (is_numeric($group_id) && $group_id > 0) {
$group = dbFetchRow('SELECT * FROM `device_groups` WHERE `id` = ? LIMIT 1', array($group_id));
$group_split = preg_split('/([a-zA-Z0-9_\-\.\=\%\<\>\ \"\'\!\~\(\)\*\/\@\[\]\^\$]+[&&\|\|]+)/', $group['pattern'], -1, (PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY));
$count = (count($group_split) - 1);
if (preg_match('/\&\&$/', $group_split[$count]) == 1 || preg_match('/\|\|$/', $group_split[$count]) == 1) {
$group_split[$count] = $group_split[$count];
} else {
$group_split[$count] = $group_split[$count].' &&';
}
$output = array(
'name' => $group['name'],
'desc' => $group['desc'],
'pattern' => $group_split,
);
header('Content-type: application/json');
echo _json_encode($output);
}

View File

@ -1,71 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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.
*/
use LibreNMS\Authentication\LegacyAuth;
if (!LegacyAuth::user()->hasGlobalAdmin()) {
die('ERROR: You need to be admin');
}
?>
<div class="modal fade" id="confirm-delete" 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">&times;</button>
<h5 class="modal-title" id="Delete">Confirm Delete</h5>
</div>
<div class="modal-body">
<p>If you would like to remove the device group then please click Delete.</p>
</div>
<div class="modal-footer">
<form role="form" class="remove_token_form">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger danger" id="device-group-removal" data-target="device-group-removal">Delete</button>
<input type="hidden" name="group_id" id="group_id" value="">
<input type="hidden" name="confirm" id="confirm" value="yes">
</form>
</div>
</div>
</div>
</div>
<script>
$('#confirm-delete').on('show.bs.modal', function(e) {
group_id = $(e.relatedTarget).data('group_id');
$("#group_id").val(group_id);
});
$('#device-group-removal').click('', function(event) {
event.preventDefault();
var group_id = $("#group_id").val();
$.ajax({
type: 'POST',
url: 'ajax_form.php',
data: { type: "delete-device-group", group_id: group_id },
dataType: "html",
success: function(msg) {
if(msg.indexOf("ERROR:") <= -1) {
$("#row_"+group_id).remove();
}
$("#message").html('<div class="alert alert-info">'+msg+'</div>');
$("#confirm-delete").modal('hide');
},
error: function() {
$("#message").html('<div class="alert alert-info">The device group could not be deleted.</div>');
$("#confirm-delete").modal('hide');
}
});
});
</script>

View File

@ -1,252 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* 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.
*/
use LibreNMS\Authentication\LegacyAuth;
if (LegacyAuth::user()->hasGlobalAdmin()) {
?>
<div class="modal fade bs-example-modal-sm" id="create-group" tabindex="-1" role="dialog" aria-labelledby="Create" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h5 class="modal-title" id="Create">Device Groups</h5>
</div>
<div class="modal-body">
<form method="post" role="form" id="devices-group" class="form-horizontal group-form">
<div class="form-group">
<div class="col-sm-12">
<span id="ajax_response"></span>
</div>
</div>
<div class='form-group'>
<label for='name' class='col-sm-3 control-label'>Name: </label>
<div class='col-sm-9'>
<input type='text' id='name' name='name' class='form-control has-feedback' maxlength='200'>
</div>
</div>
<div class='form-group'>
<label for='desc' class='col-sm-3 control-label'>Description: </label>
<div class='col-sm-9'>
<input type='text' id='desc' name='desc' class='form-control has-feedback' maxlength='200'>
</div>
</div>
<input type="hidden" name="group_id" id="group_id" value="">
<input type="hidden" name="type" id="type" value="create-device-group">
<div class="form-group">
<label for='pattern' class='col-sm-3 control-label'>Pattern: </label>
<div class="col-sm-5">
<input type='text' id='suggest' name='pattern' class='form-control has-feedback' placeholder='I.e: devices.status'/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<p>Start typing for suggestions, use '.' for indepth selection</p>
</div>
</div>
<div class="form-group">
<label for='condition' class='col-sm-3 control-label'>Condition: </label>
<div class="col-sm-5">
<select id='condition' name='condition' placeholder='Condition' class='form-control has-feedback'>
<option value='='>Equals</option>
<option value='!='>Not Equals</option>
<option value='~'>Like</option>
<option value='!~'>Not Like</option>
<option value='>'>Larger than</option>
<option value='>='>Larger than or Equals</option>
<option value='<'>Smaller than</option>
<option value='<='>Smaller than or Equals</option>
</select>
</div>
</div>
<div class="form-group">
<label for='value' class='col-sm-3 control-label'>Value: </label>
<div class="col-sm-5">
<input type='text' id='value' name='value' class='form-control has-feedback'/>
</div>
</div>
<div class="form-group">
<label for='group-glue' class='col-sm-3 control-label'>Connection: </label>
<div class="col-sm-5">
<button class="btn btn-warning btn-sm" type="submit" name="group-glue" value="&&" id="and" name="and">And</button>
<button class="btn btn-warning btn-sm" type="submit" name="group-glue" value="||" id="or" name="or">Or</button>
<span id="next-step-and"></span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<span id="response"></span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-3">
<button class="btn btn-success btn-sm" type="submit" name="group-submit" id="group-submit" value="save">Save Group</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
$('#create-group').on('hide.bs.modal', function (event) {
$('#response').data('tagmanager').empty();
$('#name').val('');
$('#desc').val('');
});
$('#create-group').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var group_id = button.data('group_id');
var modal = $(this)
$('#group_id').val(group_id);
$('#tagmanager').tagmanager();
$('#response').tagmanager({
strategy: 'array',
tagFieldName: 'patterns[]'
});
if (group_id > 0) {
$.ajax({
type: "POST",
url: "ajax_form.php",
data: {type: "parse-device-group", group_id: group_id},
dataType: "json",
success: function (output) {
var arr = [];
$.each(output['pattern'], function (key, value) {
arr.push(value);
});
$('#response').data('tagmanager').populate(arr);
$('#name').val(output['name']);
$('#desc').val(output['desc']);
}
});
}
});
var cache = {};
var suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: "ajax_rulesuggest.php?device_id=-1&term=%QUERY",
filter: function (output) {
return $.map(output, function (item) {
return {
name: item.name,
};
});
},
wildcard: "%QUERY"
}
});
suggestions.initialize();
$('#suggest').typeahead({
hint: true,
highlight: true,
minLength: 1,
classNames: {
menu: 'typeahead-left'
}
},
{
source: suggestions.ttAdapter(),
async: true,
displayKey: 'name',
valueKey: name,
templates: {
suggestion: Handlebars.compile('<p>&nbsp;{{name}}</p>')
}
});
$('#and, #or').click('', function(e) {
e.preventDefault();
$("#next-step-and").html("");
var entity = $('#suggest').val();
var condition = $('#condition').val();
var value = $('#value').val();
var glue = $(this).val();
if(entity != '' && condition != '') {
$('#response').tagmanager({
strategy: 'array',
tagFieldName: 'patterns[]'
});
if(value.indexOf("%") < 0 && isNaN(value)) {
value = '"'+value+'"';
}
if(entity.indexOf("%") < 0) {
entity = '%'+entity;
}
$('#response').data('tagmanager').populate([ entity+' '+condition+' '+value+' '+glue ]);
}
});
$('#group-submit').click('', function(e) {
e.preventDefault();
$.ajax({
type: "POST",
url: "ajax_form.php",
data: $('form.group-form').serialize(),
success: function(msg){
if(msg.indexOf("ERROR:") <= -1) {
$("#message").html('<div class="alert alert-info">'+msg+'</div>');
$("#create-group").modal('hide');
$('#response').data('tagmanager').empty();
setTimeout(function() {
location.reload(1);
}, 1000);
} else {
$('#ajax_response').html('<div class="alert alert-danger alert-dismissible"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>'+msg+'</div>');
}
},
error: function(){
$("#message").html('<div class="alert alert-info">An error occurred creating this group.</div>');
$("#create-group").modal('hide');
}
});
});
$( "#name, #suggest, #value" ).blur(function() {
var $this = $(this);
var name = $('#name').val();
var suggest = $('#suggest').val();
var value = $('#value').val();
if (name == "") {
$("#next-step-and").html("");
$("#suggest").closest('.form-group').removeClass('has-error');
$("#value").closest('.form-group').removeClass('has-error');
$("#name").closest('.form-group').addClass('has-error');
} else if (suggest == "") {
$("#next-step-and").html("");
$("#name").closest('.form-group').removeClass('has-error');
$("#value").closest('.form-group').removeClass('has-error');
$("#suggest").closest('.form-group').addClass('has-error');
} else if (value == "") {
$("#next-step-and").html("");
$("#name").closest('.form-group').removeClass('has-error');
$("#suggest").closest('.form-group').removeClass('has-error');
$("#value").closest('.form-group').addClass('has-error');
} else {
$("#name").closest('.form-group').removeClass('has-error');
$("#suggest").closest('.form-group').removeClass('has-error');
$("#value").closest('.form-group').removeClass('has-error');
$("#next-step-and").html('<i class="fa fa-long-arrow-left fa-col-danger"></i> Click AND / OR');
}
});
</script>
<?php
}

View File

@ -1,37 +0,0 @@
<?php
require_once 'includes/html/modal/new_device_group.inc.php';
require_once 'includes/html/modal/delete_device_group.inc.php';
$no_refresh = true;
$group_count_check = array_filter(GetDeviceGroups());
if (!empty($group_count_check)) {
echo '<div class="row"><div class="col-sm-12"><span id="message"></span></div></div>';
echo '<div class="table-responsive">';
echo '<table class="table table-condensed table-hover"><thead><tr>';
echo '<th>Name</th><th>Description</th><th>Pattern</th><th>Actions</th>';
echo '</tr></thead><tbody>';
foreach (GetDeviceGroups() as $group) {
echo '<tr id="row_'.$group['id'].'">';
echo '<td>'.$group['name'].'</td>';
echo '<td>'.$group['desc'].'</td>';
echo '<td>'.formatDeviceGroupPattern($group['pattern'], json_decode($group['params'])).'</td>';
echo '<td>';
echo "<button type='button' class='btn btn-primary btn-sm' aria-label='Edit' data-toggle='modal' data-target='#create-group' data-group_id='".$group['id']."' name='edit-device-group'";
if (!is_null($group['params'])) {
echo " disabled title='LibreNMS V2 device groups cannot be edited in LibreNMS V1'";
}
echo "><i class='fa fa-pencil' aria-hidden='true'></i></button> ";
echo "<button type='button' class='btn btn-danger btn-sm' aria-label='Delete' data-toggle='modal' data-target='#confirm-delete' data-group_id='".$group['id']."' name='delete-device-group'><i class='fa fa-trash' aria-hidden='true'></i></button>";
echo '</td>';
echo '</tr>';
}
echo '</tbody></table></div>';
} else { //if $group_count_check is empty, aka no group found, then display a message to the user.
echo "<center>Looks like no groups have been created, let's create one now. Click on <b>Create New Group</b> to create one.</center><br>";
echo "<center><button type='button' class='btn btn-primary btn-sm' aria-label='Add' data-toggle='modal' data-target='#create-group' data-group_id='' name='create-device-group'>Create new Group</button></center>";
}
if (!empty($group_count_check)) { //display create new node group when $group_count_check has a value so that the user can define more groups in the future.
echo "<hr>";
echo "<center><button type='button' class='btn btn-primary btn-sm' aria-label='Add' data-toggle='modal' data-target='#create-group' data-group_id='' name='create-device-group'>Create new Group</button></center>";
}

View File

@ -180,7 +180,7 @@ if ($format == "graph") {
}
if (!empty($vars['group'])) {
$where .= " AND ( ";
foreach (GetDevicesFromGroup($vars['group']) as $dev) {
foreach (DB::table('device_group_device')->where('device_group_id', $vars['group'])->pluck('device_id') as $dev) {
$where .= "device_id = ? OR ";
$sql_param[] = $dev;
}

View File

@ -72,7 +72,6 @@ if (module_selected('mocksnmp', $init_modules)) {
require_once $install_dir . '/includes/services.inc.php';
require_once $install_dir . '/includes/functions.php';
require_once $install_dir . '/includes/rewrites.php';
require_once $install_dir . '/includes/device-groups.inc.php';
if (module_selected('web', $init_modules)) {
require_once $install_dir . '/includes/html/functions.inc.php';

View File

@ -347,7 +347,14 @@ function poll_device($device, $force_module = false)
}
// Update device_groups
UpdateGroupsForDevice($device['device_id']);
echo "### Start Device Groups ###\n";
$dg_start = microtime(true);
$group_changes = \App\Models\DeviceGroup::updateGroupsFor($device['device_id']);
d_echo("Groups Added: " . implode(',', $group_changes['attached']) . PHP_EOL);
d_echo("Groups Removed: " . implode(',', $group_changes['detached']) . PHP_EOL);
echo "### End Device Groups, runtime: " . round(microtime(true) - $dg_start, 4) . "s ### \n\n";
if (!$force_module && !empty($graphs)) {
echo "Enabling graphs: ";

View File

@ -516,9 +516,10 @@ device_groups:
Columns:
- { Field: id, Type: 'int(10) unsigned', 'Null': false, Extra: auto_increment }
- { Field: name, Type: varchar(255), 'Null': false, Extra: '', Default: '' }
- { Field: desc, Type: varchar(255), 'Null': false, Extra: '', Default: '' }
- { Field: desc, Type: varchar(255), 'Null': true, Extra: '', Default: '' }
- { Field: type, Type: varchar(16), 'Null': false, Extra: '', Default: dynamic }
- { Field: rules, Type: text, 'Null': true, Extra: '' }
- { Field: pattern, Type: text, 'Null': true, Extra: '' }
- { Field: params, Type: text, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
name: { Name: name, Columns: [name], Unique: true, Type: BTREE }

View File

@ -0,0 +1,29 @@
@extends('layouts.librenmsv1')
@section('title', __('Create Device Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('device-groups.store') }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 col-sm-12">
<legend>@lang('Create Device Group')</legend>
@include('device-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('device-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('javascript')
<script src="{{ asset('js/sql-parser.min.js') }}"></script>
<script src="{{ asset('js/query-builder.standalone.min.js') }}"></script>
@endsection

View File

@ -0,0 +1,30 @@
@extends('layouts.librenmsv1')
@section('title', __('Edit Device Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('device-groups.update', $device_group->id) }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-sm-12">
<legend>@lang('Edit Device Group'): {{ $device_group->name }}</legend>
{{ method_field('PUT') }}
@include('device-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('device-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('javascript')
<script src="{{ asset('js/sql-parser.min.js') }}"></script>
<script src="{{ asset('js/query-builder.standalone.min.js') }}"></script>
@endsection

View File

@ -0,0 +1,128 @@
<div class="form-group @if($errors->has('name')) has-error @endif">
<label for="name" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Name')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="name" name="name" value="{{ old('name', $device_group->name) }}">
<span class="help-block">{{ $errors->first('name') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('desc')) has-error @endif">
<label for="desc" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Description')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="desc" name="desc" value="{{ old('desc', $device_group->desc) }}">
<span class="help-block">{{ $errors->first('desc') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('type')) has-error @endif">
<label for="type" class="control-label col-sm-3 col-md-2">@lang('Type')</label>
<div class="col-sm-9 col-md-10">
<select class="form-control" id="type" name="type" onchange="change_dg_type(this)">
<option value="dynamic"
@if(old('type', $device_group->type) == 'dynamic') selected @endif>@lang('Dynamic')</option>
<option value="static"
@if(old('type', $device_group->type) == 'static') selected @endif>@lang('Static')</option>
</select>
<span class="help-block">{{ $errors->first('type') }}</span>
</div>
</div>
<div id="dynamic-dg-form" class="form-group @if($errors->has('rules')) has-error @endif">
<label for="pattern" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Define Rules')</label>
<div class="col-sm-9 col-md-10">
<div id="builder"></div>
<span class="help-block">{{ $errors->first('rules') }}</span>
</div>
</div>
<div id="static-dg-form" class="form-group @if($errors->has('devices')) has-error @endif" style="display: none">
<label for="devices" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Select Devices')</label>
<div class="col-sm-9 col-md-10">
<select class="form-control" id="devices" name="devices[]" multiple>
@foreach($device_group->devices as $device)
<option value="{{ $device->device_id }}" selected>{{ $device->displayName() }}</option>
@endforeach
</select>
<span class="help-block">{{ $errors->first('devices') }}</span>
</div>
</div>
<script>
function change_dg_type(select) {
var type = select.options[select.selectedIndex].value;
document.getElementById("dynamic-dg-form").style.display = (type === 'dynamic' ? 'block' : 'none');
document.getElementById("static-dg-form").style.display = (type === 'dynamic' ? 'none' : 'block');
}
change_dg_type(document.getElementById('type'));
init_select2('#devices', 'device', {multiple: true});
var builder = $('#builder').on('afterApplyRuleFlags.queryBuilder afterCreateRuleFilters.queryBuilder', function () {
$("[name$='_filter']").each(function () {
$(this).select2({
dropdownAutoWidth: true,
width: 'auto'
});
});
}).on('ruleToSQL.queryBuilder.filter', function (e, rule) {
if (rule.operator === 'regexp') {
e.value += ' \'' + rule.value + '\'';
}
}).queryBuilder({
plugins: [
'bt-tooltip-errors'
// 'not-group'
],
filters: {!! $filters !!},
operators: [
'equal', 'not_equal', 'between', 'not_between', 'begins_with', 'not_begins_with', 'contains', 'not_contains', 'ends_with', 'not_ends_with', 'is_empty', 'is_not_empty', 'is_null', 'is_not_null', 'in', 'not_in',
{type: 'less', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']},
{type: 'not_regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']}
],
lang: {
operators: {
regexp: 'regex',
not_regex: 'not regex'
}
},
sqlOperators: {
regexp: {op: 'REGEXP'},
not_regexp: {op: 'NOT REGEXP'}
},
sqlRuleOperator: {
'REGEXP': function (v) {
return {val: v, op: 'regexp'};
},
'NOT REGEXP': function (v) {
return {val: v, op: 'not_regexp'};
}
}
});
$('.device-group-form').submit(function (eventObj) {
if ($('#type').val() === 'static') {
return true;
}
if (!builder.queryBuilder('validate')) {
return false;
}
$('<input type="hidden" name="rules" />')
.attr('value', JSON.stringify(builder.queryBuilder('getRules')))
.appendTo(this);
return true;
});
</script>
<script>
var rules = {!! json_encode(old('rules') ? json_decode(old('rules')) : $device_group->rules) !!};
if (rules) {
builder.queryBuilder('setRules', rules);
}
</script>

View File

@ -0,0 +1,92 @@
@extends('layouts.librenmsv1')
@section('title', __('Device Groups'))
@section('content')
<div class="container-fluid">
<div id="manage-device-groups-panel" class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-th fa-fw fa-lg" aria-hidden="true"></i> @lang('Device Groups')
</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<a type="button" class="btn btn-primary" href="{{ route('device-groups.create') }}">
<i class="fa fa-plus"></i> @lang('New Device Group')
</a>
</div>
</div>
<div class="table-responsive">
<table id="manage-device-groups-table" class="table table-condensed table-hover">
<thead>
<tr>
<th>@lang('Name')</th>
<th>@lang('Description')</th>
<th>@lang('Type')</th>
<th>@lang('Devices')</th>
<th>@lang('Pattern')</th>
<th>@lang('Actions')</th>
</tr>
</thead>
<tbody>
@foreach($device_groups as $device_group)
<tr id="row_{{ $device_group->id }}">
<td>{{ $device_group->name }}</td>
<td>{{ $device_group->desc }}</td>
<td>{{ __(ucfirst($device_group->type)) }}</td>
<td>
<a href="{{ url("/devices/group=$device_group->id") }}">{{ $device_group->devices_count }}</a>
</td>
<td>{{ $device_group->type == 'dynamic' ? $device_group->getParser()->toSql(false) : '' }}</td>
<td>
<a type="button" class="btn btn-primary btn-sm" aria-label="@lang('Edit')"
href="{{ route('device-groups.edit', $device_group->id) }}">
<i class="fa fa-pencil" aria-hidden="true"></i></a>
<button type="button" class="btn btn-danger btn-sm" aria-label="@lang('Delete')"
onclick="delete_dg(this, '{{ $device_group->name }}', '{{ route('device-groups.destroy', $device_group->id) }}')">
<i
class="fa fa-trash" aria-hidden="true"></i></button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function delete_dg(button, name, url) {
var index = button.parentNode.parentNode.rowIndex;
if (confirm('@lang('Are you sure you want to delete ')' + name + '?')) {
$.ajax({
url: url,
type: 'DELETE',
success: function (msg) {
document.getElementById("manage-device-groups-table").deleteRow(index);
toastr.success(msg);
},
error: function () {
toastr.error('@lang('The device group could not be deleted')');
}
});
}
return false;
}
</script>
@endsection
@section('css')
<style>
.table-responsive {
padding-top: 16px
}
</style>
@endsection

View File

@ -178,12 +178,11 @@
</li>
<li role="presentation" class="divider"></li>
@endconfig
@notconfig('navbar.manage_groups.hide')
<li><a href="{{ url('device-groups') }}"><i class="fa fa-th fa-fw fa-lg"
aria-hidden="true"></i> @lang('Manage Groups')</a>
</li>
@endconfig
@can('manage', \App\Models\DeviceGroup::class)
<li><a href="{{ url('device-groups') }}"><i class="fa fa-th fa-fw fa-lg"
aria-hidden="true"></i> @lang('Manage Groups')
</a></li>
@endcan
<li><a href="{{ url('device-dependencies') }}"><i class="fa fa-group fa-fw fa-lg"></i> @lang('Device Dependencies')</a></li>
@if($show_vmwinfo)
<li><a href="{{ url('vminfo') }}"><i

View File

@ -22,6 +22,7 @@ Route::group(['middleware' => ['auth', '2fa'], 'guard' => 'auth'], function () {
});
// pages
Route::resource('device-groups', 'DeviceGroupController');
Route::get('locations', 'LocationController@index');
Route::resource('preferences', 'UserPreferencesController', ['only' => ['index', 'store']]);
Route::resource('users', 'UserController');

View File

@ -25,10 +25,11 @@
namespace LibreNMS\Tests;
use LibreNMS\Alerting\QueryBuilderFluentParser;
use LibreNMS\Alerting\QueryBuilderParser;
use LibreNMS\Config;
class QueryBuilderTest extends TestCase
class QueryBuilderTest extends LaravelTestCase
{
private $data_file = 'tests/data/misc/querybuilder.json';
@ -48,14 +49,19 @@ class QueryBuilderTest extends TestCase
* @param string $display
* @param string $sql
*/
public function testQueryConversion($legacy, $builder, $display, $sql)
public function testQueryConversion($legacy, $builder, $display, $sql, $query)
{
if (!empty($legacy)) {
// some rules don't have a legacy representation
$this->assertEquals($builder, QueryBuilderParser::fromOld($legacy)->toArray());
}
$this->assertEquals($display, QueryBuilderParser::fromJson($builder)->toSql(false));
$this->assertEquals($sql, QueryBuilderParser::fromJson($builder)->toSql());
$qb = QueryBuilderFluentParser::fromJson($builder);
$this->assertEquals($display, $qb->toSql(false));
$this->assertEquals($sql, $qb->toSql());
$qbq = $qb->toQuery();
$this->assertEquals($query[0], $qbq->toSql(), 'Fluent SQL does not match');
$this->assertEquals($query[1], $qbq->getBindings(), 'Fluent bindings do not match');
}
public function loadQueryData()

View File

@ -3,114 +3,174 @@
"",
{"condition":"OR","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"begins_with","value":"begin"}],"valid":true},
"devices.hostname LIKE 'begin%'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE 'begin%'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE 'begin%'",
["select * from `devices` where (`devices`.`hostname` LIKE ?)", ["begin%"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"not_begins_with","value":"notbegin"}],"valid":true},
"devices.hostname NOT LIKE 'notbegin%'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE 'notbegin%'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE 'notbegin%'",
["select * from `devices` where (`devices`.`hostname` NOT LIKE ?)", ["notbegin%"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"contains","value":"contains"}],"valid":true},
"devices.hostname LIKE '%contains%'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE '%contains%'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE '%contains%'",
["select * from `devices` where (`devices`.`hostname` LIKE ?)", ["%contains%"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"not_contains","value":"notcontains"}],"valid":true},
"devices.hostname NOT LIKE '%notcontains%'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE '%notcontains%'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE '%notcontains%'",
["select * from `devices` where (`devices`.`hostname` NOT LIKE ?)", ["%notcontains%"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"ends_with","value":"ends"}],"valid":true},
"devices.hostname LIKE '%ends'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE '%ends'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE '%ends'",
["select * from `devices` where (`devices`.`hostname` LIKE ?)", ["%ends"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"not_ends_with","value":"notends"}],"valid":true},
"devices.hostname NOT LIKE '%notends'",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE '%notends'"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname NOT LIKE '%notends'",
["select * from `devices` where (`devices`.`hostname` NOT LIKE ?)", ["%notends"]]
],
[
"",
{"condition":"AND","rules":[{"id":"ports.ifDescr","field":"ports.ifDescr","type":"string","input":"text","operator":"is_null","value":""}],"valid":true},
"ports.ifDescr IS NULL",
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND ports.ifDescr IS NULL"
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND ports.ifDescr IS NULL",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` where (`ports`.`ifDescr` is null)", []]
],
[
"",
{"condition":"AND","rules":[{"id":"ports.ifDescr","field":"ports.ifDescr","type":"string","input":"text","operator":"is_not_null","value":""}],"valid":true},
"ports.ifDescr IS NOT NULL",
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND ports.ifDescr IS NOT NULL"
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND ports.ifDescr IS NOT NULL",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` where (`ports`.`ifDescr` is not null)", []]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hardware","field":"devices.hardware","type":"string","input":"text","operator":"is_empty","value":null}],"valid":true},
"devices.hardware = ''",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hardware = ''"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hardware = ''",
["select * from `devices` where (`devices`.`hardware` = ?)", [""]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hardware","field":"devices.hardware","type":"string","input":"text","operator":"is_not_empty","value":null}],"valid":true},
"devices.hardware != ''",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hardware != ''"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hardware != ''",
["select * from `devices` where (`devices`.`hardware` != ?)", [""]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.device_id","field":"devices.device_id","type":"integer","input":"number","operator":"between","value":["3","99"]}],"valid":true},
"devices.device_id BETWEEN 3 AND 99",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.device_id BETWEEN 3 AND 99"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.device_id BETWEEN 3 AND 99",
["select * from `devices` where (`devices`.`device_id` between ? and ?)", ["3","99"]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.device_id","field":"devices.device_id","type":"integer","input":"number","operator":"not_between","value":["2","4"]}],"valid":true},
"devices.device_id NOT BETWEEN 2 AND 4",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.device_id NOT BETWEEN 2 AND 4"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.device_id NOT BETWEEN 2 AND 4",
["select * from `devices` where (`devices`.`device_id` not between ? and ?)", ["2","4"]]
],
[
"%macros.device_up = 1",
{"condition":"AND","rules":[{"id":"macros.device_up","field":"macros.device_up","type":"integer","input":"radio","operator":"equal","value":"1"}],"valid":true},
"macros.device_up = 1",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND (devices.status = 1 && (devices.disabled = 0 && devices.ignore = 0)) = 1"
"SELECT * FROM devices WHERE (devices.device_id = ?) AND (devices.status = 1 && (devices.disabled = 0 && devices.ignore = 0)) = 1",
["select * from `devices` where ((devices.status = 1 && (devices.disabled = 0 && devices.ignore = 0)) = ?)", ["1"]]
],
[
"%sensors.sensor_current > %sensors.sensor_limit",
{"condition":"AND","rules":[{"id":"sensors.sensor_current","field":"sensors.sensor_current","type":"string","input":"text","operator":"greater","value":"`sensors.sensor_limit`"}],"valid":true},
"sensors.sensor_current > sensors.sensor_limit",
"SELECT * FROM devices,sensors WHERE (devices.device_id = ? AND devices.device_id = sensors.device_id) AND sensors.sensor_current > sensors.sensor_limit"
"SELECT * FROM devices,sensors WHERE (devices.device_id = ? AND devices.device_id = sensors.device_id) AND sensors.sensor_current > sensors.sensor_limit",
["select * from `devices` left join `sensors` on `devices`.`device_id` = `sensors`.`device_id` where (`sensors`.`sensor_current` > sensors.sensor_limit)", []]
],
[
"%devices.hostname ~ \"@ocal@\" ",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"regex","value":".*ocal.*"}],"valid":true},
"devices.hostname REGEXP \".*ocal.*\"",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname REGEXP \".*ocal.*\""
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname REGEXP \".*ocal.*\"",
["select * from `devices` where (`devices`.`hostname` REGEXP ?)", [".*ocal.*"]]
],
[
"%macros.state_sensor_critical",
{"condition":"AND","rules":[{"id":"macros.state_sensor_critical","field":"macros.state_sensor_critical","type":"integer","input":"radio","operator":"equal","value":"1"}],"valid":true},
"macros.state_sensor_critical = 1",
"SELECT * FROM devices,sensors,sensors_to_state_indexes,state_indexes,state_translations WHERE (devices.device_id = ? AND devices.device_id = sensors.device_id AND sensors.sensor_id = sensors_to_state_indexes.sensor_id AND sensors_to_state_indexes.state_index_id = state_indexes.state_index_id AND state_indexes.state_index_id = state_translations.state_index_id) AND (sensors.sensor_current = state_translations.state_value && state_translations.state_generic_value = 2) = 1"
"SELECT * FROM devices,sensors,sensors_to_state_indexes,state_indexes,state_translations WHERE (devices.device_id = ? AND devices.device_id = sensors.device_id AND sensors.sensor_id = sensors_to_state_indexes.sensor_id AND sensors_to_state_indexes.state_index_id = state_indexes.state_index_id AND state_indexes.state_index_id = state_translations.state_index_id) AND (sensors.sensor_current = state_translations.state_value && state_translations.state_generic_value = 2) = 1",
["select * from `devices` left join `sensors` on `devices`.`device_id` = `sensors`.`device_id` left join `sensors_to_state_indexes` on `sensors`.`sensor_id` = `sensors_to_state_indexes`.`sensor_id` left join `state_indexes` on `sensors_to_state_indexes`.`state_index_id` = `state_indexes`.`state_index_id` left join `state_translations` on `state_indexes`.`state_index_id` = `state_translations`.`state_index_id` where ((sensors.sensor_current = state_translations.state_value && state_translations.state_generic_value = 2) = ?)", ["1"]]
],
[
"",
{"condition":"AND","rules":[{"id":"macros.device","field":"macros.device","type":"integer","input":"radio","operator":"equal","value":"1"},{"condition":"OR","rules":[{"id":"ports.ifName","field":"ports.ifName","type":"string","input":"text","operator":"equal","value":"Ge12"},{"id":"ports.ifName","field":"ports.ifName","type":"string","input":"text","operator":"equal","value":"Ge13"}]},{"id":"ports.ifInOctets_delta","field":"ports.ifInOctets_delta","type":"integer","input":"number","operator":"greater","value":"42"}],"valid":true},
"macros.device = 1 AND (ports.ifName = \"Ge12\" OR ports.ifName = \"Ge13\") AND ports.ifInOctets_delta > 42",
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND (devices.disabled = 0 && devices.ignore = 0) = 1 AND (ports.ifName = \"Ge12\" OR ports.ifName = \"Ge13\") AND ports.ifInOctets_delta > 42"
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND (devices.disabled = 0 && devices.ignore = 0) = 1 AND (ports.ifName = \"Ge12\" OR ports.ifName = \"Ge13\") AND ports.ifInOctets_delta > 42",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` where ((devices.disabled = 0 && devices.ignore = 0) = ? AND (`ports`.`ifName` = ? OR `ports`.`ifName` = ?) AND `ports`.`ifInOctets_delta` > ?)", ["1","Ge12","Ge13","42"]]
],
[
"%bills.bill_name = \"Neil's Bill\"",
{"condition":"AND","rules":[{"id":"bills.bill_name","field":"bills.bill_name","type":"string","input":"text","operator":"equal","value":"Neil's Bill"}],"valid":true},
"bills.bill_name = \"Neil's Bill\"",
"SELECT * FROM devices,ports,bill_ports,bills WHERE (devices.device_id = ? AND devices.device_id = ports.device_id AND ports.port_id = bill_ports.port_id AND bill_ports.bill_id = bills.bill_id) AND bills.bill_name = \"Neil's Bill\""
"SELECT * FROM devices,ports,bill_ports,bills WHERE (devices.device_id = ? AND devices.device_id = ports.device_id AND ports.port_id = bill_ports.port_id AND bill_ports.bill_id = bills.bill_id) AND bills.bill_name = \"Neil's Bill\"",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` left join `bill_ports` on `ports`.`port_id` = `bill_ports`.`port_id` left join `bills` on `bill_ports`.`bill_id` = `bills`.`bill_id` where (`bills`.`bill_name` = ?)", ["Neil's Bill"]]
],
[
"%ports.ifOutErrors_rate >= \"100\" || %ports.ifInErrors_rate >= \"100\"",
{"condition":"OR","rules":[{"id":"ports.ifOutErrors_rate","field":"ports.ifOutErrors_rate","type":"string","input":"text","operator":"greater_or_equal","value":"100"},{"id":"ports.ifInErrors_rate","field":"ports.ifInErrors_rate","type":"string","input":"text","operator":"greater_or_equal","value":"100"}],"valid":true},
"ports.ifOutErrors_rate >= 100 OR ports.ifInErrors_rate >= 100",
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND (ports.ifOutErrors_rate >= 100 OR ports.ifInErrors_rate >= 100)"
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND (ports.ifOutErrors_rate >= 100 OR ports.ifInErrors_rate >= 100)",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` where (`ports`.`ifOutErrors_rate` >= ? OR `ports`.`ifInErrors_rate` >= ?)", ["100","100"]]
],
[
"%devices.hostname = %devices.sysName &&",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"equal","value":"`devices.sysName`"}],"valid":true},
"devices.hostname = devices.sysName",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname = devices.sysName",
["select * from `devices` where (`devices`.`hostname` = devices.sysName)", []]
],
[
"%devices.hostname ~ \"%router\" &&",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"regex","value":"router"}],"valid":true},
"devices.hostname REGEXP \"router\"",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname REGEXP \"router\"",
["select * from `devices` where (`devices`.`hostname` REGEXP ?)", ["router"]]
],
[
"syslog.timestamp > %macros.past_5m && syslog.msg ~ \"@Port Down@\"",
{"condition":"AND","rules":[{"id":"syslog.timestamp","field":"syslog.timestamp","type":"datetime","input":"text","operator":"greater","value":"`macros.past_5m`"},{"id":"syslog.msg","field":"syslog.msg","type":"string","input":"text","operator":"regex","value":".*Port Down.*"}],"valid":true},
"syslog.timestamp > macros.past_5m AND syslog.msg REGEXP \".*Port Down.*\"",
"SELECT * FROM devices,syslog WHERE (devices.device_id = ? AND devices.device_id = syslog.device_id) AND syslog.timestamp > (DATE_SUB(NOW(),INTERVAL 5 MINUTE)) AND syslog.msg REGEXP \".*Port Down.*\"",
["select * from `devices` left join `syslog` on `devices`.`device_id` = `syslog`.`device_id` where (`syslog`.`timestamp` > (DATE_SUB(NOW(),INTERVAL 5 MINUTE)) AND `syslog`.`msg` REGEXP ?)", [".*Port Down.*"]]
],
[
"%macros.port_usage_perc > 80",
{"condition":"AND","rules":[{"id":"macros.port_usage_perc","field":"macros.port_usage_perc","type":"integer","input":"text","operator":"greater","value":"80"}],"valid":true},
"macros.port_usage_perc > 80",
"SELECT * FROM devices,ports WHERE (devices.device_id = ? AND devices.device_id = ports.device_id) AND (((ports.ifInOctets_rate*8) / ports.ifSpeed)*100) > 80",
["select * from `devices` left join `ports` on `devices`.`device_id` = `ports`.`device_id` where ((((ports.ifInOctets_rate*8) / ports.ifSpeed)*100) > ?)", ["80"]]],
[
"%devices.sysName ~ \"..domain.com\" || %devices.sysName ~ \"switch.\" &&",
{"condition":"OR","rules":[{"id":"devices.sysName","field":"devices.sysName","type":"string","input":"text","operator":"regex","value":"..domain.com"},{"id":"devices.sysName","field":"devices.sysName","type":"string","input":"text","operator":"regex","value":"switch."}],"valid":true},
"devices.sysName REGEXP \"..domain.com\" OR devices.sysName REGEXP \"switch.\"",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND (devices.sysName REGEXP \"..domain.com\" OR devices.sysName REGEXP \"switch.\")",
["select * from `devices` where (`devices`.`sysName` REGEXP ? OR `devices`.`sysName` REGEXP ?)", ["..domain.com","switch."]]
],
[
"",
{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"contains","value":"one"},{"condition":"OR","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"contains","value":"two"},{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"contains","value":"three"},{"condition":"AND","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"begins_with","value":"six"},{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"equal","value":"seven"}]}]},{"condition":"OR","rules":[{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"not_contains","value":"four"},{"id":"devices.hostname","field":"devices.hostname","type":"string","input":"text","operator":"not_contains","value":"five"}]}],"valid":true},
"devices.hostname LIKE '%one%' AND (devices.hostname LIKE '%two%' OR devices.hostname LIKE '%three%' OR (devices.hostname LIKE 'six%' AND devices.hostname = \"seven\")) AND (devices.hostname NOT LIKE '%four%' OR devices.hostname NOT LIKE '%five%')",
"SELECT * FROM devices WHERE (devices.device_id = ?) AND devices.hostname LIKE '%one%' AND (devices.hostname LIKE '%two%' OR devices.hostname LIKE '%three%' OR (devices.hostname LIKE 'six%' AND devices.hostname = \"seven\")) AND (devices.hostname NOT LIKE '%four%' OR devices.hostname NOT LIKE '%five%')",
["select * from `devices` where (`devices`.`hostname` LIKE ? AND (`devices`.`hostname` LIKE ? OR `devices`.`hostname` LIKE ? OR (`devices`.`hostname` LIKE ? AND `devices`.`hostname` = ?)) AND (`devices`.`hostname` NOT LIKE ? OR `devices`.`hostname` NOT LIKE ?))", ["%one%","%two%","%three%","six%","seven","%four%","%five%"]]
]
]