diff --git a/LibreNMS/Services.php b/LibreNMS/Services.php new file mode 100644 index 000000000000..a74e71f8e685 --- /dev/null +++ b/LibreNMS/Services.php @@ -0,0 +1,48 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS; + +class Services +{ + /** + * List all available services from nagios plugins directory + * + * @return array + */ + public static function list() + { + $services = []; + if (is_dir(Config::get('nagios_plugins'))) { + foreach (scandir(Config::get('nagios_plugins')) as $file) { + if (substr($file, 0, 6) === 'check_') { + $services[] = substr($file, 6); + } + } + } + + return $services; + } +} diff --git a/app/Http/Controllers/DeviceGroupController.php b/app/Http/Controllers/DeviceGroupController.php index c54befb63245..b4d5fa4f5f39 100644 --- a/app/Http/Controllers/DeviceGroupController.php +++ b/app/Http/Controllers/DeviceGroupController.php @@ -175,6 +175,11 @@ public function update(Request $request, DeviceGroup $deviceGroup) */ public function destroy(DeviceGroup $deviceGroup) { + if ($deviceGroup->serviceTemplates()->exists()) { + $msg = __('Device Group :name still has Service Templates associated with it. Please remove or update the Service Template accordingly', ['name' => $deviceGroup->name]); + + return response($msg, 200); + } $deviceGroup->delete(); $msg = __('Device Group :name deleted', ['name' => $deviceGroup->name]); diff --git a/app/Http/Controllers/Select/ServiceTemplateController.php b/app/Http/Controllers/Select/ServiceTemplateController.php new file mode 100644 index 000000000000..8c62cc290c28 --- /dev/null +++ b/app/Http/Controllers/Select/ServiceTemplateController.php @@ -0,0 +1,48 @@ +. + * + * @link http://librenms.org + * @copyright 2020 Anthony F McInerney + * @author Anthony F McInerney + */ + +namespace App\Http\Controllers\Select; + +use App\Models\ServiceTemplate; + +class ServiceTemplateController extends SelectController +{ + protected function searchFields($request) + { + return ['name']; + } + + protected function baseQuery($request) + { + return ServiceTemplate::hasAccess($request->user())->select('id', 'name'); + } + + public function formatItem($template) + { + return [ + 'id' => $template->id, + 'text' => $template->name, + ]; + } +} diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php new file mode 100644 index 000000000000..a6271fc50377 --- /dev/null +++ b/app/Http/Controllers/ServiceController.php @@ -0,0 +1,52 @@ + 'required|string|unique:service', + 'device_id' => 'integer', + 'service_type' => 'string', + 'service_param' => 'nullable|string', + 'service_ip' => 'nullable|string', + 'service_desc' => 'nullable|string', + 'service_changed' => 'integer', + 'service_disabled' => 'integer', + 'service_ignore' => 'integer', + ]; + + $service = Service::make( + $request->only( + [ + 'service_name', + 'device_id', + 'service_type', + 'service_param', + 'service_ip', + 'service_desc', + 'service_changed', + 'service_disabled', + 'service_ignore', + ] + ) + ); + $service->save(); + + Toastr::success(__('Service :name created', ['name' => $service->service_name])); + + return redirect()->route('services.templates.index'); + } +} diff --git a/app/Http/Controllers/ServiceTemplateController.php b/app/Http/Controllers/ServiceTemplateController.php new file mode 100644 index 000000000000..6a0807074fc1 --- /dev/null +++ b/app/Http/Controllers/ServiceTemplateController.php @@ -0,0 +1,378 @@ +authorizeResource(ServiceTemplate::class, 'template'); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response|\Illuminate\View\View + */ + public function index() + { + //$this->authorize('manage', ServiceTemplate::class); + + return view( + 'service-template.index', [ + 'service_templates' => ServiceTemplate::orderBy('name')->withCount('devices')->withCount('groups')->get(), + 'groups' => DeviceGroup::orderBy('name')->has('serviceTemplates')->get(), + 'devices' => Device::orderBy('hostname')->has('serviceTemplates')->get(), + ] + ); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response|\Illuminate\View\View + */ + public function create() + { + return view( + 'service-template.create', [ + 'template' => new ServiceTemplate(), + 'service_templates' => ServiceTemplate::orderBy('name')->get(), + 'services' => Services::list(), + 'filters' => json_encode(new QueryBuilderFilter('group')), + ] + ); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function store(Request $request) + { + $this->validate( + $request, [ + 'name' => 'required|string|unique:service_templates', + 'groups' => 'array', + 'groups.*' => 'integer', + 'devices' => 'array', + 'devices.*' => 'integer', + 'type' => 'string', + 'dtype' => 'required|in:dynamic,static', + 'dgtype' => 'required|in:dynamic,static', + 'drules' => 'json|required_if:dtype,dynamic', + 'dgrules' => 'json|required_if:dgtype,dynamic', + 'param' => 'nullable|string', + 'ip' => 'nullable|string', + 'desc' => 'nullable|string', + 'changed' => 'integer', + 'disabled' => 'integer', + 'ignore' => 'integer', + ] + ); + + $template = ServiceTemplate::make( + $request->only( + [ + 'name', + 'type', + 'dtype', + 'dgtype', + 'drules', + 'dgrules', + 'param', + 'ip', + 'desc', + 'changed', + 'disabled', + 'ignore', + ] + ) + ); + $template->drules = json_decode($request->drules); + $template->dgrules = json_decode($request->dgrules); + $template->save(); + + if ($request->dtype == 'static') { + $template->devices()->sync($request->devices); + } + if ($request->dgtype == 'static') { + $template->groups()->sync($request->groups); + } + Toastr::success(__('Service Template :name created', ['name' => $template->name])); + + return redirect()->route('services.templates.index'); + } + + /** + * Display the specified resource. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function show(ServiceTemplate $template) + { + return redirect(url('/services/templates/' . $template->id)); + } + + /** + * Show the form for editing the specified resource. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\Response|\Illuminate\View\View + */ + public function edit(ServiceTemplate $template) + { + return view( + 'service-template.edit', [ + 'template' => $template, + 'filters' => json_encode(new QueryBuilderFilter('group')), + 'services' => Services::list(), + ] + // + ); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function update(Request $request, ServiceTemplate $template) + { + $this->validate( + $request, [ + 'name' => [ + 'required', + 'string', + Rule::unique('service_templates')->where( + function ($query) use ($template) { + $query->where('id', '!=', $template->id); + } + ), + ], + 'dtype' => 'required|in:dynamic,static', + 'drules' => 'json|required_if:dtype,dynamic', + 'devices' => 'array', + 'devices.*' => 'integer', + 'dgtype' => 'required|in:dynamic,static', + 'dgrules' => 'json|required_if:dgtype,dynamic', + 'groups' => 'array', + 'groups.*' => 'integer', + 'type' => 'string', + 'param' => 'nullable|string', + 'ip' => 'nullable|string', + 'desc' => 'nullable|string', + 'changed' => 'integer', + 'disabled' => 'integer', + 'ignore' => 'integer', + ] + ); + + $template->fill( + $request->only( + [ + 'name', + 'type', + 'dtype', + 'dgtype', + 'drules', + 'dgrules', + 'param', + 'ip', + 'desc', + 'changed', + 'ignore', + 'disabled', + ] + ) + ); + + $devices_updated = false; + if ($template->dtype == 'static') { + // sync device_ids from input + $updated = $template->devices()->sync($request->get('devices', [])); + // check for attached/detached/updated + $devices_updated = array_sum(array_map(function ($device_ids) { + return count($device_ids); + }, $updated)) > 0; + } else { + $template->drules = json_decode($request->drules); + } + + $device_groups_updated = false; + if ($template->dgtype == 'static') { + // sync device_group_ids from input + $updated = $template->groups()->sync($request->get('groups', [])); + // check for attached/detached/updated + $device_groups_updated = array_sum(array_map(function ($device_group_ids) { + return count($device_group_ids); + }, $updated)) > 0; + } else { + $template->dgrules = json_decode($request->dgrules); + } + + if ($template->isDirty() || $devices_updated || $device_groups_updated) { + try { + if ($template->save() || $devices_updated || $device_groups_updated) { + Toastr::success(__('Service Template :name updated', ['name' => $template->name])); + } else { + Toastr::error(__('Failed to save')); + + return redirect()->back()->withInput(); + } + } catch (\Illuminate\Database\QueryException $e) { + return redirect()->back()->withInput()->withErrors([ + 'drules' => __('Rules resulted in invalid query: ') . $e->getMessage(), + 'dgrules' => __('Rules resulted in invalid query: ') . $e->getMessage(), + ]); + } + } else { + Toastr::info(__('No changes made')); + } + + return redirect()->route('services.templates.index'); + } + + /** + * Apply specified Service Template to Device Groups. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function applyDeviceGroups(ServiceTemplate $template) + { + foreach (DeviceGroup::inServiceTemplate($template->id)->get() as $device_group) { + foreach (Device::inDeviceGroup($device_group->id)->get() as $device) { + $device->services()->updateOrCreate( + [ + 'service_template_id' => $template->id, + ], + [ + 'service_name' => $template->name, + 'service_type' => $template->type, + 'service_template_id' => $template->id, + 'service_param' => $template->param, + 'service_ip' => $template->ip, + 'service_desc' => $template->desc, + 'service_disabled' => $template->disabled, + 'service_ignore' => $template->ignore, + ] + ); + } + } + $msg = __('Services for Template :name have been updated', ['name' => $template->name]); + + return response($msg, 200); + } + + /** + * Apply specified Service Template to Devices. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function applyDevices(ServiceTemplate $template) + { + foreach (Device::inServiceTemplate($template->id)->get() as $device) { + $device->services()->updateOrCreate( + [ + 'service_template_id' => $template->id, + ], + [ + 'service_name' => $template->name, + 'service_type' => $template->type, + 'service_template_id' => $template->id, + 'service_param' => $template->param, + 'service_ip' => $template->ip, + 'service_desc' => $template->desc, + 'service_disabled' => $template->disabled, + 'service_ignore' => $template->ignore, + ] + ); + } + $msg = __('Services for Template :name have been updated', ['name' => $template->name]); + + return response($msg, 200); + } + + /** + * Apply all Service Templates. + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function applyAll() + { + foreach (ServiceTemplate::all() as $template) { + ServiceTemplateController::apply($template); + } + $msg = __('All Service Templates have been applied'); + + return response($msg, 200); + } + + /** + * Apply specified Service Template. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function apply(ServiceTemplate $template) + { + ServiceTemplateController::applyDevices($template); + ServiceTemplateController::applyDeviceGroups($template); + + // remove any remaining services no longer in the correct device group + foreach (Device::notInServiceTemplate($template->id)->notInDeviceGroup($template->groups)->get() as $device) { + Service::where('device_id', $device->device_id)->where('service_template_id', $template->id)->delete(); + } + $msg = __('All Service Templates have been applied'); + + return response($msg, 200); + } + + /** + * Remove specified Service Template. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function remove(ServiceTemplate $template) + { + Service::where('service_template_id', $template->id)->delete(); + + $msg = __('All Service Templates have been applied'); + + return response($msg, 200); + } + + /** + * Destroy the specified resource from storage. + * + * @param \App\Models\ServiceTemplate $template + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View + */ + public function destroy(ServiceTemplate $template) + { + Service::where('service_template_id', $template->id)->delete(); + $template->delete(); + + $msg = __('Service Template :name deleted, Services removed', ['name' => $template->name]); + + return response($msg, 200); + } +} diff --git a/app/Models/Device.php b/app/Models/Device.php index 41715eb56a47..42e09bf718cd 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -456,11 +456,46 @@ public function scopeHasAccess($query, User $user) public function scopeInDeviceGroup($query, $deviceGroup) { - return $query->whereIn($query->qualifyColumn('device_id'), function ($query) use ($deviceGroup) { - $query->select('device_id') - ->from('device_group_device') - ->where('device_group_id', $deviceGroup); - }); + return $query->whereIn( + $query->qualifyColumn('device_id'), function ($query) use ($deviceGroup) { + $query->select('device_id') + ->from('device_group_device') + ->where('device_group_id', $deviceGroup); + } + ); + } + + public function scopeNotInDeviceGroup($query, $deviceGroup) + { + return $query->whereNotIn( + $query->qualifyColumn('device_id'), function ($query) use ($deviceGroup) { + $query->select('device_id') + ->from('device_group_device') + ->where('device_group_id', $deviceGroup); + } + ); + } + + public function scopeInServiceTemplate($query, $serviceTemplate) + { + return $query->whereIn( + $query->qualifyColumn('device_id'), function ($query) use ($serviceTemplate) { + $query->select('device_id') + ->from('service_templates_device') + ->where('service_template_id', $serviceTemplate); + } + ); + } + + public function scopeNotInServiceTemplate($query, $serviceTemplate) + { + return $query->whereNotIn( + $query->qualifyColumn('device_id'), function ($query) use ($serviceTemplate) { + $query->select('device_id') + ->from('service_templates_device') + ->where('service_template_id', $serviceTemplate); + } + ); } // ---- Define Relationships ---- @@ -635,6 +670,11 @@ public function sensors() return $this->hasMany(\App\Models\Sensor::class, 'device_id'); } + public function serviceTemplates() + { + return $this->belongsToMany(\App\Models\ServiceTemplate::class, 'service_templates_device', 'device_id', 'service_template_id'); + } + public function services() { return $this->hasMany(\App\Models\Service::class, 'device_id'); diff --git a/app/Models/DeviceGroup.php b/app/Models/DeviceGroup.php index 9fde99c17773..e00022c927df 100644 --- a/app/Models/DeviceGroup.php +++ b/app/Models/DeviceGroup.php @@ -138,6 +138,28 @@ public function scopeHasAccess($query, User $user) return $query->whereIn('id', Permissions::deviceGroupsForUser($user)); } + public function scopeInServiceTemplate($query, $serviceTemplate) + { + return $query->whereIn( + $query->qualifyColumn('id'), function ($query) use ($serviceTemplate) { + $query->select('device_group_id') + ->from('service_templates_device_group') + ->where('service_template_id', $serviceTemplate); + } + ); + } + + public function scopeNotInServiceTemplate($query, $serviceTemplate) + { + return $query->whereNotIn( + $query->qualifyColumn('id'), function ($query) use ($serviceTemplate) { + $query->select('device_group_id') + ->from('service_templates_device_group') + ->where('service_template_id', $serviceTemplate); + } + ); + } + // ---- Define Relationships ---- public function devices() @@ -154,4 +176,9 @@ public function users() { return $this->belongsToMany(\App\Models\User::class, 'devices_group_perms', 'device_group_id', 'user_id'); } + + public function serviceTemplates() + { + return $this->belongsToMany(\App\Models\ServiceTemplate::class, 'service_templates_device_group', 'device_group_id', 'service_template_id'); + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index c25b9e2d3afb..d931d1adeebf 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -8,6 +8,22 @@ class Service extends DeviceRelatedModel { public $timestamps = false; protected $primaryKey = 'service_id'; + protected $fillable = [ + 'service_id', + 'device_id', + 'service_ip', + 'service_type', + 'service_desc', + 'service_param', + 'service_ignore', + 'service_status', + 'service_changed', + 'service_message', + 'service_disabled', + 'service_ds', + 'service_template_id', + 'service_name', + ]; // ---- Query Scopes ---- diff --git a/app/Models/ServiceTemplate.php b/app/Models/ServiceTemplate.php new file mode 100644 index 000000000000..9fa8c13f4d0f --- /dev/null +++ b/app/Models/ServiceTemplate.php @@ -0,0 +1,258 @@ +. + * + * @link http://librenms.org + * @copyright 2020 Anthony F McInerney + * @author Anthony F McInerney + */ + +namespace App\Models; + +use Illuminate\Database\Eloquent\Builder; +use LibreNMS\Alerting\QueryBuilderFluentParser; +use Log; + +class ServiceTemplate extends BaseModel +{ + public $timestamps = false; + protected $primaryKey = 'id'; + protected $fillable = [ + 'id', + 'ip', + 'type', + 'dtype', + 'dgtype', + 'drules', + 'dgrules', + 'desc', + 'param', + 'ignore', + 'status', + 'changed', + 'disabled', + 'name', + ]; + + protected $attributes = [ // default values + 'ignore' => '0', + 'disabled' => '0', + ]; + + protected $casts = [ + 'ignore' => 'integer', + 'disabled' => 'integer', + 'drules' => 'array', + 'dgrules' => 'array', + ]; + + public static function boot() + { + parent::boot(); + + static::deleting(function (ServiceTemplate $template) { + $template->devices()->detach(); + $template->groups()->detach(); + }); + + static::saving(function (ServiceTemplate $template) { + if ($template->isDirty('drules')) { + $template->drules = $template->getDeviceParser()->generateJoins()->toArray(); + } + if ($template->isDirty('dgrules')) { + $template->dgrules = $template->getDeviceGroupParser()->generateJoins()->toArray(); + } + }); + + static::saved(function (ServiceTemplate $template) { + if ($template->isDirty('drules')) { + $template->updateDevices(); + } + if ($template->isDirty('dgrules')) { + $template->updateGroups(); + } + }); + } + + // ---- Helper Functions ---- + + /** + * Update devices included in this template (dynamic only) + */ + public function updateDevices() + { + if ($this->dtype == 'dynamic') { + $this->devices()->sync(QueryBuilderFluentParser::fromJSON($this->rules)->toQuery() + ->distinct()->pluck('devices.device_id')); + } + } + + /** + * Update device groups included in this template (dynamic only) + */ + public function updateGroups() + { + if ($this->dgtype == 'dynamic') { + $this->groups()->sync(QueryBuilderFluentParser::fromJSON($this->rules)->toQuery() + ->distinct()->pluck('device_groups.id')); + } + } + + /** + * Update the device template groups for the given device or device_id + * + * @param Device|int $device + * @return array + */ + public static function updateServiceTemplatesForDevice($device) + { + $device = ($device instanceof Device ? $device : Device::find($device)); + if (! $device instanceof Device) { + // could not load device + return [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + } + + $template_ids = static::query() + ->with(['devices' => function ($query) { + $query->select('devices.device_id'); + }]) + ->get() + ->filter(function ($template) use ($device) { + /** @var ServiceTemplate $template */ + if ($template->dtype == 'dynamic') { + try { + return $template->getDeviceParser() + ->toQuery() + ->where('devices.device_id', $device->device_id) + ->exists(); + } catch (\Illuminate\Database\QueryException $e) { + Log::error("Service Template '$template->name' generates invalid query: " . $e->getMessage()); + + return false; + } + } + + // for static, if this device is include, keep it. + return $template->devices + ->where('device_id', $device->device_id) + ->isNotEmpty(); + })->pluck('id'); + + return $device->serviceTemplates()->sync($template_ids); + } + + /** + * Update the device template groups for the given device group or device_group_id + * + * @param DeviceGroup|int $deviceGroup + * @return array + */ + public static function updateServiceTemplatesForDeviceGroup($deviceGroup) + { + $deviceGroup = ($deviceGroup instanceof DeviceGroup ? $deviceGroup : DeviceGroup::find($deviceGroup)); + if (! $deviceGroup instanceof DeviceGroup) { + // could not load device + return [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + } + + $template_ids = static::query() + ->with(['device_groups' => function ($query) { + $query->select('device_groups.id'); + }]) + ->get() + ->filter(function ($template) use ($deviceGroup) { + /** @var ServiceTemplate $template */ + if ($template->dgtype == 'dynamic') { + try { + return $template->getDeviceGroupParser() + ->toQuery() + ->where('device_groups.id', $deviceGroup->id) + ->exists(); + } catch (\Illuminate\Database\QueryException $e) { + Log::error("Service Template '$template->name' generates invalid query: " . $e->getMessage()); + + return false; + } + } + + // for static, if this device group is include, keep it. + return $template->groups + ->where('device_group_id', $deviceGroup->id) + ->isNotEmpty(); + })->pluck('id'); + + return $deviceGroup->serviceTemplates()->sync($template_ids); + } + + /** + * Get a query builder parser instance from this Service Template device rule + * + * @return QueryBuilderFluentParser + */ + public function getDeviceParser() + { + return QueryBuilderFluentParser::fromJson($this->drules); + } + + /** + * Get a query builder parser instance from this Service Template device group rule + * + * @return QueryBuilderFluentParser + */ + public function getDeviceGroupParser() + { + return QueryBuilderFluentParser::fromJson($this->dgrules); + } + + // ---- Query Scopes ---- + + /** + * @param Builder $query + * @return Builder + */ + public function scopeIsDisabled($query) + { + return $query->where('disabled', 1); + } + + // ---- Define Relationships ---- + + public function devices() + { + return $this->belongsToMany(\App\Models\Device::class, 'service_templates_device', 'service_template_id', 'device_id'); + } + + public function services() + { + return $this->belongsToMany(\App\Models\Service::class, 'service_templates_device', 'service_template_id', 'device_id'); + } + + public function groups() + { + return $this->belongsToMany(\App\Models\DeviceGroup::class, 'service_templates_device_group', 'service_template_id', 'device_group_id'); + } +} diff --git a/app/Observers/ServiceObserver.php b/app/Observers/ServiceObserver.php new file mode 100644 index 000000000000..c40b213cad5d --- /dev/null +++ b/app/Observers/ServiceObserver.php @@ -0,0 +1,63 @@ +hasGlobalRead(); + } + + /** + * Determine whether the user can view the service template. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function view(User $user, ServiceTemplate $template) + { + return $this->viewAny($user); + } + + /** + * Determine whether the user can create service templates. + * + * @param \App\Models\User $user + * @return mixed + */ + public function create(User $user) + { + return $user->hasGlobalAdmin(); + } + + /** + * Determine whether the user can update the service template. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function update(User $user, ServiceTemplate $template) + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the service template. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function delete(User $user, ServiceTemplate $template) + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the service template. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function restore(User $user, ServiceTemplate $template) + { + return $user->hasGlobalAdmin(); + } + + /** + * Determine whether the user can permanently delete the service template. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function forceDelete(User $user, ServiceTemplate $template) + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can view the stored configuration of the service template + * from Oxidized or Rancid + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function showConfig(User $user, ServiceTemplate $template) + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update service template notes. + * + * @param \App\Models\User $user + * @param \App\Models\ServiceTemplate $template + * @return mixed + */ + public function updateNotes(User $user, ServiceTemplate $template) + { + return $user->isAdmin(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 73313760c1c3..298d6b4dbc7f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -130,6 +130,7 @@ private function registerGeocoder() private function bootObservers() { \App\Models\Device::observe(\App\Observers\DeviceObserver::class); + \App\Models\Service::observe(\App\Observers\ServiceObserver::class); } private function bootCustomValidators() diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 42a140554b2d..5f4e2e77944e 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -20,6 +20,7 @@ class AuthServiceProvider extends ServiceProvider \App\Models\DeviceGroup::class => \App\Policies\DeviceGroupPolicy::class, \App\Models\PollerCluster::class => \App\Policies\PollerClusterPolicy::class, \App\Models\Port::class => \App\Policies\PortPolicy::class, + \App\Models\ServiceTemplate::class => \App\Policies\ServiceTemplatePolicy::class, ]; /** diff --git a/database/migrations/2020_09_18_230114_create_service_templates_device_group_table.php b/database/migrations/2020_09_18_230114_create_service_templates_device_group_table.php new file mode 100644 index 000000000000..a33408e45a28 --- /dev/null +++ b/database/migrations/2020_09_18_230114_create_service_templates_device_group_table.php @@ -0,0 +1,32 @@ +unsignedInteger('service_template_id')->unsigned()->index(); + $table->unsignedInteger('device_group_id')->unsigned()->index(); + + $table->primary(['service_template_id', 'device_group_id'], 'service_templates_device_group_relationship_primary'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('service_templates_device_group'); + } +} diff --git a/database/migrations/2020_09_18_230114_create_service_templates_device_table.php b/database/migrations/2020_09_18_230114_create_service_templates_device_table.php new file mode 100644 index 000000000000..f3afb1810967 --- /dev/null +++ b/database/migrations/2020_09_18_230114_create_service_templates_device_table.php @@ -0,0 +1,32 @@ +unsignedInteger('service_template_id')->unsigned()->index(); + $table->unsignedInteger('device_id')->unsigned()->index(); + + $table->primary(['service_template_id', 'device_id'], 'service_templates_device_relationship_primary'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('service_templates_device'); + } +} diff --git a/database/migrations/2020_09_18_230114_create_service_templates_table.php b/database/migrations/2020_09_18_230114_create_service_templates_table.php new file mode 100644 index 000000000000..b58f6ee4177d --- /dev/null +++ b/database/migrations/2020_09_18_230114_create_service_templates_table.php @@ -0,0 +1,45 @@ +increments('id'); + $table->text('ip')->nullable()->default(null); + $table->string('type'); + $table->string('dtype', 16)->default('static'); + $table->string('dgtype', 16)->default('static'); + $table->text('drules')->nullable(); + $table->text('dgrules')->nullable(); + $table->text('desc')->nullable()->default(null); + $table->text('param')->nullable()->default(null); + $table->boolean('ignore')->default(0); + if (\LibreNMS\DB\Eloquent::getDriver() == 'mysql') { + $table->timestamp('changed')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); + } else { + $table->timestamp('changed')->useCurrent(); + } + $table->boolean('disabled')->default(0); + $table->string('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('service_templates'); + } +} diff --git a/database/migrations/2020_09_18_230114_extend_services_table_for_service_templates_table.php b/database/migrations/2020_09_18_230114_extend_services_table_for_service_templates_table.php new file mode 100644 index 000000000000..fb3d15d31718 --- /dev/null +++ b/database/migrations/2020_09_18_230114_extend_services_table_for_service_templates_table.php @@ -0,0 +1,47 @@ +unsignedInteger('service_template_id')->default(0); + $table->string('service_name')->nullable()->default(null); + $table->text('service_desc')->nullable()->default(null)->change(); + $table->text('service_param')->nullable()->default(null)->change(); + $table->boolean('service_ignore')->default(0)->change(); + $table->text('service_ip')->nullable()->default(null)->change(); + $table->text('service_ds')->nullable()->default(null)->change(); + $table->text('service_message')->nullable()->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn([ + 'service_template_id', + 'service_name', + ]); + $table->text('service_desc')->change(); + $table->text('service_param')->change(); + $table->boolean('service_ignore')->change(); + $table->text('service_ip')->change(); + $table->text('service_ds')->change(); + $table->text('service_message')->change(); + }); + } +} diff --git a/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_group_table.php b/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_group_table.php new file mode 100644 index 000000000000..44e3a5945490 --- /dev/null +++ b/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_group_table.php @@ -0,0 +1,35 @@ +foreign('service_template_id')->references('id')->on('service_templates')->onUpdate('RESTRICT')->onDelete('CASCADE'); + $table->foreign('device_group_id')->references('id')->on('device_groups')->onUpdate('RESTRICT')->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if (\LibreNMS\DB\Eloquent::getDriver() !== 'sqlite') { + Schema::table('service_templates_device_group', function (Blueprint $table) { + $table->dropForeign('service_templates_device_group_service_template_id_foreign'); + $table->dropForeign('service_templates_device_group_device_group_id_foreign'); + }); + } + } +} diff --git a/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_table.php b/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_table.php new file mode 100644 index 000000000000..4126f55922d7 --- /dev/null +++ b/database/migrations/2020_09_19_230114_add_foreign_keys_to_service_templates_device_table.php @@ -0,0 +1,35 @@ +foreign('service_template_id')->references('id')->on('service_templates')->onUpdate('RESTRICT')->onDelete('CASCADE'); + $table->foreign('device_id')->references('device_id')->on('devices')->onUpdate('RESTRICT')->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if (\LibreNMS\DB\Eloquent::getDriver() !== 'sqlite') { + Schema::table('service_templates_device', function (Blueprint $table) { + $table->dropForeign('service_templates_device_service_template_id_foreign'); + $table->dropForeign('service_templates_device_device_id_foreign'); + }); + } + } +} diff --git a/doc/Extensions/Services.md b/doc/Extensions/Services.md index fc3429a633c4..c7dfc0cb3380 100644 --- a/doc/Extensions/Services.md +++ b/doc/Extensions/Services.md @@ -23,6 +23,54 @@ Note: Plugins will only load if they are prefixed with `check_`. The `check_` prefix is stripped out when displaying in the "Add Service" GUI "Type" dropdown list. +## Service Templates + +Service Templates within LibreNMS provides the same ability as Nagios +does with Host Groups. Known as Device Groups in LibreNMS. +They are applied devices that belong to the specified Device Group. + +Use the Apply buttons to manually create or update Services for the Service +Template. +Use the Remove buttons to manually remove Services for the Service Template. + +After you Edit a Service Template, and then use Apply, all relevant changes are +pushed to existing Services previously created. + +You can also enable Service Templates Auto Discovery to have Services +added / removed / updated on regular discover intervals. + +When a Device is a member of multiple Device Groups, templates from +all of those Device Groups are applied. + +If a Device is added or removed from a Device Group, when the Apply button +is used or Auto Discovery runs Services will be added / removed as +appropriate. + +**Service Templates are tied into Device Groups, you need at least +one Device Group to be able to add Service Templates - You can define a +dummy one. The Device Group does not need members to add Service Templates.** + +## Service Auto Discovery + +To automatically create services for devices with available checks. + +You need to enable the discover services within config.php with the following: + +```php +$config['discover_services'] = true; +``` + +## Service Templates Auto Discovery + +To automatically create services for devices with configured +Service Templates. + +You need to enable the discover services within config.php with the following: + +```php +$config['discover_services_templates'] = true; +``` + ## Setup Service checks are now distributable if you run a distributed @@ -157,6 +205,42 @@ then you can run the following command to help troubleshoot services. ./check-services.php -d ``` +## Related Polling / Discovery Options + +These settings are related and should be investigated and set accordingly. +The below values are not defaults or recommended. + +```php +$config['service_poller_enabled'] = true; +``` +```php +$config['service_poller_workers'] = 16; +``` +```php +$config['service_poller_frequency'] = 300; +``` +```php +$config['service_poller_down_retry'] = 5; +``` +```php +$config['service_discovery_enabled'] = true; +``` +```php +$config['service_discovery_workers'] = 16; +``` +```php +$config['service_discovery_frequency'] = 3600; +``` +```php +$config['service_services_enabled'] = true; +``` +```php +$config['service_services_workers'] = 16; +``` +```php +$config['service_services_frequency'] = 60; +``` + ## Service checks polling logic Service check is skipped when the associated device is not pingable, diff --git a/includes/discovery/services.inc.php b/includes/discovery/services.inc.php index 75056bb72d43..c561517bbac6 100644 --- a/includes/discovery/services.inc.php +++ b/includes/discovery/services.inc.php @@ -1,7 +1,11 @@ getContent(), true); + if (json_last_error() || ! is_array($data)) { + return api_error(400, "We couldn't parse the provided json. " . json_last_error_msg()); + } + + $rules = [ + 'name' => 'required|string|unique:service_templates', + 'device_group_id' => 'integer', + 'type' => 'string', + 'param' => 'nullable|string', + 'ip' => 'nullable|string', + 'desc' => 'nullable|string', + 'changed' => 'integer', + 'disabled' => 'integer', + 'ignore' => 'integer', + ]; + + $v = Validator::make($data, $rules); + if ($v->fails()) { + return api_error(422, $v->messages()); + } + + // Only use the rules if they are able to be parsed by the QueryBuilder + $query = QueryBuilderParser::fromJson($data['rules'])->toSql(); + if (empty($query)) { + return api_error(500, "We couldn't parse your rule"); + } + + $serviceTemplate = ServiceTemplate::make(['name' => $data['name'], 'device_group_id' => $data['device_group_id'], 'type' => $data['type'], 'param' => $data['param'], 'ip' => $data['ip'], 'desc' => $data['desc'], 'changed' => $data['changed'], 'disabled' => $data['disabled'], 'ignore' => $data['ignore']]); + $serviceTemplate->save(); + + return api_success($serviceTemplate->id, 'id', 'Service Template ' . $serviceTemplate->name . ' created', 201); +} + +function get_service_templates(Illuminate\Http\Request $request) +{ + if ($request->user()->cannot('viewAny', ServiceTemplate::class)) { + return api_error(403, 'Insufficient permissions to access service templates'); + } + + $templates = ServiceTemplate::query()->orderBy('name')->get(); + + if ($templates->isEmpty()) { + return api_error(404, 'No service templates found'); + } + + return api_success($templates->makeHidden('pivot')->toArray(), 'templates', 'Found ' . $templates->count() . ' service templates'); +} + function add_service_for_host(Illuminate\Http\Request $request) { $hostname = $request->route('hostname'); @@ -2344,7 +2396,9 @@ function add_service_for_host(Illuminate\Http\Request $request) $service_desc = $data['desc'] ? $data['desc'] : ''; $service_param = $data['param'] ? $data['param'] : ''; $service_ignore = $data['ignore'] ? true : false; // Default false - $service_id = add_service($device_id, $service_type, $service_desc, $service_ip, $service_param, (int) $service_ignore); + $service_disable = $data['disable'] ? true : false; // Default false + $service_name = $data['name']; + $service_id = add_service($device_id, $service_type, $service_desc, $service_ip, $service_param, (int) $service_ignore, (int) $service_disable, 0, $service_name); if ($service_id != false) { return api_success_noresult(201, "Service $service_type has been added to device $hostname (#$service_id)"); } diff --git a/includes/html/forms/create-service.inc.php b/includes/html/forms/create-service.inc.php index 34ee239a0494..62b88b0d1c9d 100644 --- a/includes/html/forms/create-service.inc.php +++ b/includes/html/forms/create-service.inc.php @@ -24,10 +24,12 @@ $ignore = isset($vars['ignore']) ? 1 : 0; $disabled = isset($vars['disabled']) ? 1 : 0; $device_id = $vars['device_id']; +$template_id = $vars['template_id']; +$name = $vars['name']; if (is_numeric($service_id) && $service_id > 0) { // Need to edit. - $update = ['service_desc' => $desc, 'service_ip' => $ip, 'service_param' => $param, 'service_ignore' => $ignore, 'service_disabled' => $disabled]; + $update = ['service_desc' => $desc, 'service_ip' => $ip, 'service_param' => $param, 'service_ignore' => $ignore, 'service_disabled' => $disabled, 'service_template_id' => $template_id, 'service_name' => $name]; if (is_numeric(edit_service($update, $service_id))) { $status = ['status' =>0, 'message' => 'Modified Service: ' . $service_id . ': ' . $type . '']; } else { @@ -35,7 +37,7 @@ } } else { // Need to add. - $service_id = add_service($device_id, $type, $desc, $ip, $param, $ignore, $disabled); + $service_id = add_service($device_id, $type, $desc, $ip, $param, $ignore, $disabled, 0, $name); if ($service_id == false) { $status = ['status' =>1, 'message' => 'ERROR: Failed to add Service: ' . $type . '']; } else { diff --git a/includes/html/forms/parse-service.inc.php b/includes/html/forms/parse-service.inc.php index a989f5d34ff2..544a654c9ecd 100644 --- a/includes/html/forms/parse-service.inc.php +++ b/includes/html/forms/parse-service.inc.php @@ -27,7 +27,9 @@ 'desc' => $service[0]['service_desc'], 'param' => $service[0]['service_param'], 'ignore' => $service[0]['service_ignore'], - 'disabled' => $service[0]['service_disabled'], + 'disabled' => $service[0]['service_disabled'], + 'template_id' => $service[0]['service_template_id'], + 'name' => $service[0]['service_name'], ]; header('Content-Type: application/json'); diff --git a/includes/html/modal/new_service.inc.php b/includes/html/modal/new_service.inc.php index 3a78d1a1c007..2ddd165efbb7 100644 --- a/includes/html/modal/new_service.inc.php +++ b/includes/html/modal/new_service.inc.php @@ -30,64 +30,88 @@