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 629182915d..b8e91eaeb5 100644 Binary files a/doc/img/device_groups.png and b/doc/img/device_groups.png differ diff --git a/html/ajax_rulesuggest.php b/html/ajax_rulesuggest.php deleted file mode 100644 index a04973a83c..0000000000 --- a/html/ajax_rulesuggest.php +++ /dev/null @@ -1,138 +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 . - */ - -/* - * 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') +
+
+
+ @lang('Create Device Group') + + @include('device-group.form') + +
+
+ + @lang('Cancel') +
+
+
+
+
+@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%"]] ] ]