From 1a60c44eb0ca1b6e41829de2114aae7152f99943 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 19 Jun 2019 16:01:53 -0500 Subject: [PATCH] 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 --- LibreNMS/Alerting/QueryBuilderFilter.php | 1 - .../Alerting/QueryBuilderFluentParser.php | 188 +++++++++ LibreNMS/Alerting/QueryBuilderParser.php | 39 +- LibreNMS/DB/Schema.php | 2 +- .../Controllers/DeviceGroupController.php | 173 ++++++++ app/Models/DeviceGroup.php | 307 ++++----------- app/Policies/DeviceGroupPolicy.php | 101 +++++ app/Providers/AuthServiceProvider.php | 5 +- daily.php | 21 + daily.sh | 1 + ...019_05_30_225937_device_groups_rewrite.php | 38 ++ doc/Extensions/Device-Groups.md | 31 +- doc/img/device_groups.png | Bin 51276 -> 57903 bytes html/ajax_rulesuggest.php | 138 ------- includes/common.php | 18 - includes/defaults.inc.php | 3 - includes/device-groups.inc.php | 371 ------------------ includes/helpers.php | 20 + includes/html/api_functions.inc.php | 51 ++- includes/html/common/alerts.inc.php | 7 +- .../html/forms/create-device-group.inc.php | 53 --- .../html/forms/delete-device-group.inc.php | 36 -- .../html/forms/parse-device-group.inc.php | 41 -- .../html/modal/delete_device_group.inc.php | 71 ---- includes/html/modal/new_device_group.inc.php | 252 ------------ includes/html/pages/device-groups.inc.php | 37 -- includes/html/pages/devices.inc.php | 2 +- includes/init.php | 1 - includes/polling/functions.inc.php | 9 +- misc/db_schema.yaml | 5 +- resources/views/device-group/create.blade.php | 29 ++ resources/views/device-group/edit.blade.php | 30 ++ resources/views/device-group/form.blade.php | 128 ++++++ resources/views/device-group/index.blade.php | 92 +++++ resources/views/layouts/menu.blade.php | 11 +- routes/web.php | 1 + tests/QueryBuilderTest.php | 14 +- tests/data/misc/querybuilder.json | 98 ++++- 38 files changed, 1065 insertions(+), 1360 deletions(-) create mode 100644 LibreNMS/Alerting/QueryBuilderFluentParser.php create mode 100644 app/Http/Controllers/DeviceGroupController.php create mode 100644 app/Policies/DeviceGroupPolicy.php create mode 100644 database/migrations/2019_05_30_225937_device_groups_rewrite.php delete mode 100644 html/ajax_rulesuggest.php delete mode 100644 includes/device-groups.inc.php delete mode 100644 includes/html/forms/create-device-group.inc.php delete mode 100644 includes/html/forms/delete-device-group.inc.php delete mode 100644 includes/html/forms/parse-device-group.inc.php delete mode 100644 includes/html/modal/delete_device_group.inc.php delete mode 100644 includes/html/modal/new_device_group.inc.php delete mode 100644 includes/html/pages/device-groups.inc.php create mode 100644 resources/views/device-group/create.blade.php create mode 100644 resources/views/device-group/edit.blade.php create mode 100644 resources/views/device-group/form.blade.php create mode 100644 resources/views/device-group/index.blade.php diff --git a/LibreNMS/Alerting/QueryBuilderFilter.php b/LibreNMS/Alerting/QueryBuilderFilter.php index 721a1c6cf0..edd173b07b 100644 --- a/LibreNMS/Alerting/QueryBuilderFilter.php +++ b/LibreNMS/Alerting/QueryBuilderFilter.php @@ -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 diff --git a/LibreNMS/Alerting/QueryBuilderFluentParser.php b/LibreNMS/Alerting/QueryBuilderFluentParser.php new file mode 100644 index 0000000000..660fd57aca --- /dev/null +++ b/LibreNMS/Alerting/QueryBuilderFluentParser.php @@ -0,0 +1,188 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2019 Tony Murray + * @author Tony Murray + */ + +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; + } +} diff --git a/LibreNMS/Alerting/QueryBuilderParser.php b/LibreNMS/Alerting/QueryBuilderParser.php index c2435e9750..ede26f9baf 100644 --- a/LibreNMS/Alerting/QueryBuilderParser.php +++ b/LibreNMS/Alerting/QueryBuilderParser.php @@ -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); } /** diff --git a/LibreNMS/DB/Schema.php b/LibreNMS/DB/Schema.php index b0c213fb3f..21d37ffa07 100644 --- a/LibreNMS/DB/Schema.php +++ b/LibreNMS/DB/Schema.php @@ -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)) { diff --git a/app/Http/Controllers/DeviceGroupController.php b/app/Http/Controllers/DeviceGroupController.php new file mode 100644 index 0000000000..14b52475e6 --- /dev/null +++ b/app/Http/Controllers/DeviceGroupController.php @@ -0,0 +1,173 @@ +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'); + } +} diff --git a/app/Models/DeviceGroup.php b/app/Models/DeviceGroup.php index 69167f5db4..a758ceec6e 100644 --- a/app/Models/DeviceGroup.php +++ b/app/Models/DeviceGroup.php @@ -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'); diff --git a/app/Policies/DeviceGroupPolicy.php b/app/Policies/DeviceGroupPolicy.php new file mode 100644 index 0000000000..c916cd919f --- /dev/null +++ b/app/Policies/DeviceGroupPolicy.php @@ -0,0 +1,101 @@ +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; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index e32f71579f..755e911e4d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -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, ]; /** diff --git a/daily.php b/daily.php index 798bac1e2b..ce1d95da03 100644 --- a/daily.php +++ b/daily.php @@ -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( diff --git a/daily.sh b/daily.sh index 76785272fa..8b48214db5 100755 --- a/daily.sh +++ b/daily.sh @@ -262,6 +262,7 @@ main () { # Cleanups local options=("refresh_alert_rules" "refresh_os_cache" + "refresh_device_groups" "recalculate_device_dependencies" "syslog" "eventlog" diff --git a/database/migrations/2019_05_30_225937_device_groups_rewrite.php b/database/migrations/2019_05_30_225937_device_groups_rewrite.php new file mode 100644 index 0000000000..944966e8a2 --- /dev/null +++ b/database/migrations/2019_05_30_225937_device_groups_rewrite.php @@ -0,0 +1,38 @@ +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'); + }); + } +} diff --git a/doc/Extensions/Device-Groups.md b/doc/Extensions/Device-Groups.md index fb5377d6c9..e58b6d551b 100644 --- a/doc/Extensions/Device-Groups.md +++ b/doc/Extensions/Device-Groups.md @@ -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 `. +### 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 `. -#### 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) diff --git a/doc/img/device_groups.png b/doc/img/device_groups.png index 629182915d2ec7850464e48e658ab8b43daffd43..b8e91eaeb5521dc3559e4b85e558cfa6126889cd 100644 GIT binary patch literal 57903 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 g0|$#Z!Cg - * 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 . - */ - -/* - * Rule Suggestion-AJAX - * @author Daniel Preussker - * @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)); diff --git a/includes/common.php b/includes/common.php index 60995a183b..9c40418c21 100644 --- a/includes/common.php +++ b/includes/common.php @@ -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; -} diff --git a/includes/defaults.inc.php b/includes/defaults.inc.php index b20fe97b2c..c58db88bd6 100644 --- a/includes/defaults.inc.php +++ b/includes/defaults.inc.php @@ -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; diff --git a/includes/device-groups.inc.php b/includes/device-groups.inc.php deleted file mode 100644 index dc5b054d9c..0000000000 --- a/includes/device-groups.inc.php +++ /dev/null @@ -1,371 +0,0 @@ - - * 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 . - */ - -/** - * Device-Grouping - * @author Daniel Preussker - * @author Tony Murray - * @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; -} diff --git a/includes/helpers.php b/includes/helpers.php index d854c5bae0..1368e71814 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -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; + } +} diff --git a/includes/html/api_functions.inc.php b/includes/html/api_functions.inc.php index 2ffe9ec544..c7a408b7ff 100644 --- a/includes/html/api_functions.inc.php +++ b/includes/html/api_functions.inc.php @@ -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'); diff --git a/includes/html/common/alerts.inc.php b/includes/html/common/alerts.inc.php index c385d421f2..a9f5cd245b 100644 --- a/includes/html/common/alerts.inc.php +++ b/includes/html/common/alerts.inc.php @@ -115,11 +115,8 @@ if (defined('SHOW_SETTINGS')) { diff --git a/includes/html/forms/create-device-group.inc.php b/includes/html/forms/create-device-group.inc.php deleted file mode 100644 index bea867b191..0000000000 --- a/includes/html/forms/create-device-group.inc.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * 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: $name: $pattern"; - } else { - $update_message = 'ERROR: Failed to edit Group: '.$pattern.''; - } -} else { - if (AddDeviceGroup($name, $desc, $pattern)) { - $update_message = "Added Group: $name: $pattern"; - } else { - $update_message = 'ERROR: Failed to add Group: '.$pattern.''; - } -} - -echo $update_message; diff --git a/includes/html/forms/delete-device-group.inc.php b/includes/html/forms/delete-device-group.inc.php deleted file mode 100644 index c5afd7a31f..0000000000 --- a/includes/html/forms/delete-device-group.inc.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * 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; - } -} diff --git a/includes/html/forms/parse-device-group.inc.php b/includes/html/forms/parse-device-group.inc.php deleted file mode 100644 index ad628a3084..0000000000 --- a/includes/html/forms/parse-device-group.inc.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * 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); -} diff --git a/includes/html/modal/delete_device_group.inc.php b/includes/html/modal/delete_device_group.inc.php deleted file mode 100644 index c5f84c0183..0000000000 --- a/includes/html/modal/delete_device_group.inc.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * 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'); -} - -?> - -

- - diff --git a/includes/html/modal/new_device_group.inc.php b/includes/html/modal/new_device_group.inc.php deleted file mode 100644 index 58de02a1e4..0000000000 --- a/includes/html/modal/new_device_group.inc.php +++ /dev/null @@ -1,252 +0,0 @@ - - * - * 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()) { -?> - - - - - -
'; - echo '
'; - echo ''; - echo ''; - echo ''; - foreach (GetDeviceGroups() as $group) { - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
NameDescriptionPatternActions
'.$group['name'].''.$group['desc'].''.formatDeviceGroupPattern($group['pattern'], json_decode($group['params'])).''; - echo " "; - echo ""; - echo '
'; -} else { //if $group_count_check is empty, aka no group found, then display a message to the user. - echo "
Looks like no groups have been created, let's create one now. Click on Create New Group to create one.

"; - echo "
"; -} - -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 "
"; - echo "
"; -} diff --git a/includes/html/pages/devices.inc.php b/includes/html/pages/devices.inc.php index 75a9172ff2..e44d53af5e 100644 --- a/includes/html/pages/devices.inc.php +++ b/includes/html/pages/devices.inc.php @@ -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; } diff --git a/includes/init.php b/includes/init.php index be7c6ce003..d0c9819439 100644 --- a/includes/init.php +++ b/includes/init.php @@ -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'; diff --git a/includes/polling/functions.inc.php b/includes/polling/functions.inc.php index 716200ce3a..9503e6a387 100644 --- a/includes/polling/functions.inc.php +++ b/includes/polling/functions.inc.php @@ -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: "; diff --git a/misc/db_schema.yaml b/misc/db_schema.yaml index a572a16d36..62cd3104ce 100644 --- a/misc/db_schema.yaml +++ b/misc/db_schema.yaml @@ -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 } diff --git a/resources/views/device-group/create.blade.php b/resources/views/device-group/create.blade.php new file mode 100644 index 0000000000..1fb0d0c115 --- /dev/null +++ b/resources/views/device-group/create.blade.php @@ -0,0 +1,29 @@ +@extends('layouts.librenmsv1') + +@section('title', __('Create Device Group')) + +@section('content') +
+@endsection + +@section('javascript') + + +@endsection diff --git a/resources/views/device-group/edit.blade.php b/resources/views/device-group/edit.blade.php new file mode 100644 index 0000000000..44905b970a --- /dev/null +++ b/resources/views/device-group/edit.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.librenmsv1') + +@section('title', __('Edit Device Group')) + +@section('content') +
+
+
+ @lang('Edit Device Group'): {{ $device_group->name }} + {{ method_field('PUT') }} + + @include('device-group.form') + +
+
+ + @lang('Cancel') +
+
+
+
+
+@endsection + +@section('javascript') + + +@endsection diff --git a/resources/views/device-group/form.blade.php b/resources/views/device-group/form.blade.php new file mode 100644 index 0000000000..6c7b136c29 --- /dev/null +++ b/resources/views/device-group/form.blade.php @@ -0,0 +1,128 @@ +
+ +
+ + {{ $errors->first('name') }} +
+
+ +
+ +
+ + {{ $errors->first('desc') }} +
+
+ +
+ +
+ + {{ $errors->first('type') }} +
+
+ +
+ +
+
+ {{ $errors->first('rules') }} +
+
+ + + + + diff --git a/resources/views/device-group/index.blade.php b/resources/views/device-group/index.blade.php new file mode 100644 index 0000000000..32b3f1a122 --- /dev/null +++ b/resources/views/device-group/index.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.librenmsv1') + +@section('title', __('Device Groups')) + +@section('content') +
+
+
+

+ @lang('Device Groups') +

+
+
+ +
+ + + + + + + + + + + + + @foreach($device_groups as $device_group) + + + + + + + + + @endforeach + +
@lang('Name')@lang('Description')@lang('Type')@lang('Devices')@lang('Pattern')@lang('Actions')
{{ $device_group->name }}{{ $device_group->desc }}{{ __(ucfirst($device_group->type)) }} + id") }}">{{ $device_group->devices_count }} + {{ $device_group->type == 'dynamic' ? $device_group->getParser()->toSql(false) : '' }} + + + +
+
+
+
+
+@endsection + +@section('scripts') + +@endsection + +@section('css') + +@endsection diff --git a/resources/views/layouts/menu.blade.php b/resources/views/layouts/menu.blade.php index 2cea9a02e5..c0b5472c9a 100644 --- a/resources/views/layouts/menu.blade.php +++ b/resources/views/layouts/menu.blade.php @@ -178,12 +178,11 @@ @endconfig - - @notconfig('navbar.manage_groups.hide') -
  • @lang('Manage Groups') -
  • - @endconfig + @can('manage', \App\Models\DeviceGroup::class) +
  • @lang('Manage Groups') +
  • + @endcan
  • @lang('Device Dependencies')
  • @if($show_vmwinfo)
  • ['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'); diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 58b5ac2842..99bd635a18 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -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() diff --git a/tests/data/misc/querybuilder.json b/tests/data/misc/querybuilder.json index c93215a442..de67dd67e3 100644 --- a/tests/data/misc/querybuilder.json +++ b/tests/data/misc/querybuilder.json @@ -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%"]] ] ]