Maintenance Windows: recurring now works overnight (#11389)

* Alert Rules use Carbon
Use carbon for time queries, need to be careful of timezone quirks.  Timestamps won't have that issue

* Convert existing schedules to UTC

* we only need start end... we already have start/end

* Make AlertSchedule::isActive work

* Table display working (and using Eloquent)

* Schedule creation updated

* WIP

* use Eloquent mutators/accessors

* recurring_day mutator/accessor

* WIP UI

* Fix SQL to handle outside times

* fix up some ui issues

* Convert alert-schedule to new style
Improved sorting customization

* fix whitespace

* support sqlite

* Fix schedule creation missing id

* Fix Device::isUnderMaintenance() add location relation
This commit is contained in:
Tony Murray
2020-06-14 16:35:50 -05:00
committed by GitHub
20 changed files with 424 additions and 229 deletions

View File

@@ -34,6 +34,7 @@
namespace LibreNMS\Alert; namespace LibreNMS\Alert;
use App\Models\Device; use App\Models\Device;
use Carbon\Carbon;
use LibreNMS\Alert\AlertUtil; use LibreNMS\Alert\AlertUtil;
use LibreNMS\Alert\AlertDB; use LibreNMS\Alert\AlertDB;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
@@ -113,7 +114,7 @@ class AlertRules
if (is_null($current_state)) { if (is_null($current_state)) {
dbInsert(array('state' => AlertState::ACTIVE, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1,'alerted' => 0), 'alerts'); dbInsert(array('state' => AlertState::ACTIVE, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1,'alerted' => 0), 'alerts');
} else { } else {
dbUpdate(['state' => AlertState::ACTIVE, 'open' => 1, 'timestamp' => array('NOW()')], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); dbUpdate(['state' => AlertState::ACTIVE, 'open' => 1, 'timestamp' => Carbon::now()->timestamp], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
} }
c_echo(PHP_EOL . 'Status: %rALERT'); c_echo(PHP_EOL . 'Status: %rALERT');
} }
@@ -126,7 +127,7 @@ class AlertRules
if (is_null($current_state)) { if (is_null($current_state)) {
dbInsert(['state' => AlertState::RECOVERED, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1, 'alerted' => 0], 'alerts'); dbInsert(['state' => AlertState::RECOVERED, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1, 'alerted' => 0], 'alerts');
} else { } else {
dbUpdate(['state' => AlertState::RECOVERED, 'open' => 1, 'note' => '', 'timestamp' => array('NOW()')], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); dbUpdate(['state' => AlertState::RECOVERED, 'open' => 1, 'note' => '', 'timestamp' => Carbon::now()->timestamp], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
} }
c_echo(PHP_EOL . 'Status: %gOK'); c_echo(PHP_EOL . 'Status: %gOK');

View File

@@ -27,6 +27,7 @@ namespace LibreNMS\Alert;
use App\Models\Device; use App\Models\Device;
use App\Models\User; use App\Models\User;
use DeviceCache;
use LibreNMS\Config; use LibreNMS\Config;
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\PHPMailer;
@@ -196,8 +197,7 @@ class AlertUtil
*/ */
public static function isMaintenance($device_id) public static function isMaintenance($device_id)
{ {
$device = Device::find($device_id); return DeviceCache::get($device_id)->isUnderMaintenance();
return !is_null($device) && $device->isUnderMaintenance();
} }
/** /**

View File

@@ -453,7 +453,7 @@ class RunAlerts
$noacc = false; $noacc = false;
} }
if (AlertUtil::isMaintenance($alert['device_id']) > 0) { if (AlertUtil::isMaintenance($alert['device_id'])) {
$noiss = true; $noiss = true;
$noacc = true; $noacc = true;
} }

View File

@@ -33,6 +33,12 @@ use Illuminate\Support\Collection;
abstract class PaginatedAjaxController extends Controller abstract class PaginatedAjaxController extends Controller
{ {
/**
* Default sort, column => direction
* @var array
*/
protected $default_sort = [];
/** /**
* Base rules for this controller. * Base rules for this controller.
* *
@@ -88,6 +94,18 @@ abstract class PaginatedAjaxController extends Controller
return []; return [];
} }
/**
* Defines sortable fields. The incoming sort field should be the key, the sql column or DB::raw() should be the value
*
* @param \Illuminate\Http\Request $request
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function sortFields($request)
{
return [];
}
/** /**
* Format an item for display. Default is pass-through * Format an item for display. Default is pass-through
* *
@@ -148,9 +166,14 @@ abstract class PaginatedAjaxController extends Controller
*/ */
protected function sort($request, $query) protected function sort($request, $query)
{ {
$sort = $request->get('sort', []); $columns = $this->sortFields($request);
$sort = $request->get('sort', $this->default_sort);
foreach ($sort as $column => $direction) { foreach ($sort as $column => $direction) {
$query->orderBy($column, $direction); if (isset($columns[$column])) {
$query->orderBy($columns[$column], $direction == 'desc' ? 'desc' : 'asc');
}
} }
return $query; return $query;

View File

@@ -0,0 +1,101 @@
<?php
/**
* AlertScheduleController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers\Table;
use App\Models\AlertSchedule;
use Carbon\Carbon;
use DB;
class AlertScheduleController extends TableController
{
protected $default_sort = ['title' => 'asc', 'start' => 'asc'];
protected function baseQuery($request)
{
return AlertSchedule::query();
}
protected function searchFields($request)
{
return['title', 'start', 'end'];
}
protected function sortFields($request)
{
return [
'start_recurring_dt' => DB::raw('DATE(`start`)'),
'start_recurring_ht' => DB::raw('TIME(`start`)'),
'end_recurring_dt' => DB::raw('DATE(`end`)'),
'end_recurring_ht' => DB::raw('TIME(`end`)'),
'title' => 'title',
'recurring' => 'recurring',
'start' => 'start',
'end' => 'end',
'status' => DB::raw("end < '" . Carbon::now('UTC') ."'"), // only partition lapsed
];
}
/**
* @param AlertSchedule $schedule
* @return array
*/
public function formatItem($schedule)
{
return [
'title' => $schedule->title,
'notes' => $schedule->notes,
'id' => $schedule->schedule_id,
'start' => $schedule->recurring ? '' : $schedule->start->toDateTimeString('minutes'),
'end' => $schedule->recurring ? '' : $schedule->end->toDateTimeString('minutes'),
'start_recurring_dt' => $schedule->recurring ? $schedule->start_recurring_dt : '',
'start_recurring_hr' => $schedule->recurring ? $schedule->start_recurring_hr : '',
'end_recurring_dt' => $schedule->recurring ? $schedule->end_recurring_dt : '',
'end_recurring_hr' => $schedule->recurring ? $schedule->end_recurring_hr : '',
'recurring' => $schedule->recurring ? __('Yes') : __('No'),
'recurring_day' => $schedule->recurring ? implode(',', $schedule->recurring_day) : '',
'status' => $schedule->status,
];
}
// /**
// * @param Request $request
// * @param Builder $query
// * @return Builder
// */
// protected function sort($request, $query)
// {
// $columns = $this->sortFields($request);
// $sort = $request->get('sort', $this->default_sort);
//
// foreach ($sort as $column => $direction) {
// if (isset($columns[$column])) {
// $query->orderBy($columns[$column], $direction == 'desc' ? 'desc' : 'asc');
// }
// }
//
// return $query;
// }
}

View File

@@ -32,7 +32,17 @@ use Illuminate\Http\Request;
abstract class TableController extends PaginatedAjaxController abstract class TableController extends PaginatedAjaxController
{ {
protected $default_sort = []; protected $model;
protected function sortFields($request)
{
if (isset($this->model)) {
$fields = \Schema::getColumnListing((new $this->model)->getTable());
return array_combine($fields, $fields);
}
return [];
}
final protected function baseRules() final protected function baseRules()
{ {

View File

@@ -25,48 +25,187 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Date;
use DB; use DB;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class AlertSchedule extends Model class AlertSchedule extends Model
{ {
const SCHEDULE_SET = 0;
const SCHEDULE_ACTIVE = 2;
const SCHEDULE_LAPSED = 1;
public $timestamps = false; public $timestamps = false;
protected $table = 'alert_schedule'; protected $table = 'alert_schedule';
protected $primaryKey = 'schedule_id'; protected $primaryKey = 'schedule_id';
protected $appends = ['start_recurring_dt', 'end_recurring_dt', 'start_recurring_hr', 'end_recurring_hr', 'status'];
private $timezone;
private $days = [
'Mo' => 1,
'Tu' => 2,
'We' => 3,
'Th' => 4,
'Fr' => 5,
'Sa' => 6,
'Su' => 7,
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->timezone = config('app.timezone');
}
// ---- Accessors/Mutators ----
public function getRecurringDayAttribute()
{
return explode(',', str_replace(array_values($this->days), array_keys($this->days), $this->attributes['recurring_day']));
}
public function setRecurringDayAttribute($days)
{
$days = is_array($days) ? $days : explode(',', $days);
$new_days = [];
foreach ($days as $day) {
if (isset($this->days[$day])) {
$new_days[] = $this->days[$day];
}
}
$this->attributes['recurring_day'] = implode(',', $new_days);
}
public function getStartAttribute()
{
return Date::parse($this->attributes['start'], 'UTC')->tz($this->timezone);
}
public function setStartAttribute($start)
{
$this->attributes['start'] = $this->fromDateTime(Date::parse($start)->tz('UTC'));
}
public function getEndAttribute()
{
return Date::parse($this->attributes['end'], 'UTC')->tz($this->timezone);
}
public function setEndAttribute($end)
{
$this->attributes['end'] = $this->fromDateTime(Date::parse($end)->tz('UTC'));
}
public function getStartRecurringDtAttribute()
{
return $this->start->toDateString();
}
public function getStartRecurringHrAttribute()
{
return $this->start->toTimeString('minute');
}
public function getEndRecurringDtAttribute()
{
$end = $this->end;
return $end->year == '9000' ? null : $end->toDateString();
}
public function getEndRecurringHrAttribute()
{
return $this->end->toTimeString('minute');
}
public function setStartRecurringDtAttribute($date)
{
$this->start = $this->start->setDateFrom(Date::parse($date, $this->timezone));
}
public function setStartRecurringHrAttribute($time)
{
$this->start = $this->start->setTimeFrom(Date::parse($time, $this->timezone));
}
public function setEndRecurringDtAttribute($date)
{
$this->end = $this->end->setDateFrom(Date::parse($date ?: '9000-09-09', $this->timezone));
}
public function setEndRecurringHrAttribute($time)
{
$this->end = $this->end->setTimeFrom(Date::parse($time, $this->timezone));
}
/**
* @return int Status 0: SCHEDULE_SET, 1: SCHEDULE_LAPSED, 2: SCHEDULE_ACTIVE
*/
public function getStatusAttribute()
{
$now = Carbon::now();
if ($now > $this->end) {
return self::SCHEDULE_LAPSED;
}
if (!$this->recurring) {
return $now > $this->start ? self::SCHEDULE_ACTIVE : self::SCHEDULE_SET;
}
// recurring
$now_time = $now->secondsSinceMidnight();
$start_time = $this->start->secondsSinceMidnight();
$end_time = $this->end->secondsSinceMidnight();
$after_start = $now > $this->start;
$spans_days = $start_time > $end_time;
// check inside start and end times or outside start and end times (if we span a day)
$active = $spans_days ? ($after_start && ($now_time < $end_time || $now_time >= $start_time)) : ($now_time >= $start_time && $now_time < $end_time);
return $active && Str::contains($this->attributes['recurring_day'], $now->format('N')) ? self::SCHEDULE_ACTIVE : self::SCHEDULE_SET;
}
// ---- Query scopes ---- // ---- Query scopes ----
public function scopeIsActive($query) public function scopeIsActive($query)
{ {
// TODO use Carbon?
return $query->where(function ($query) { return $query->where(function ($query) {
$query->where(function ($query) { $now = CarbonImmutable::now('UTC');
// Non recurring simply between start and end $query->where('start', '<=', $now)
$query->where('recurring', 0) ->where('end', '>=', $now)
->where('start', '<=', DB::raw('NOW()')) ->where(function ($query) use ($now) {
->where('end', '>=', DB::raw('NOW()')); $query->where('recurring', 0) // Non recurring simply between start and end
})->orWhere(function ($query) { ->orWhere(function ($query) use ($now) {
$query->where('recurring', 1) $query->where('recurring', 1)
// Check the time is after the start date and before the end date, or end date is not set // Check the time is after the start date and before the end date, or end date is not set
->where(function ($query) { ->where(function ($query) use ($now) {
$query->where('start_recurring_dt', '<=', DB::raw("date_format(NOW(), '%Y-%m-%d')")) $query->where(function ($query) use ($now) {
->where(function ($query) { // normal, inside one day
$query->where('end_recurring_dt', '>=', DB::raw("date_format(NOW(), '%Y-%m-%d')")) $query->whereTime('start', '<', DB::raw("time(`end`)"))
->orWhereNull('end_recurring_dt'); ->whereTime('start', '<=', $now->toTimeString())
->whereTime('end', '>', $now->toTimeString());
})->orWhere(function ($query) use ($now) {
// outside, spans days
$query->whereTime('start', '>', DB::raw("time(`end`)"))
->where(function ($query) use ($now) {
$query->whereTime('end', '<=', $now->toTimeString())
->orWhereTime('start', '>', $now->toTimeString());
});
});
})
// Check we are on the correct day of the week
->where(function ($query) use ($now) {
$query->where('recurring_day', 'like', $now->format('%N%'))
->orWhereNull('recurring_day');
}); });
})
// Check the time is between the start and end hour/minutes/seconds
->where('start_recurring_hr', '<=', DB::raw("date_format(NOW(), '%H:%i:%s')"))
->where('end_recurring_hr', '>=', DB::raw("date_format(NOW(), '%H:%i:%s')"))
// Check we are on the correct day of the week
->where(function ($query) {
/** @var Builder $query */
$query->where('recurring_day', 'like', DB::raw("CONCAT('%', date_format(NOW(), '%w'), '%')"))
->orWhereNull('recurring_day')
->orWhere('recurring_day', '');
}); });
}); });
}); });
} }
@@ -74,11 +213,16 @@ class AlertSchedule extends Model
public function devices() public function devices()
{ {
return $this->morphedByMany(\App\Models\Device::class, 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'schedule_id'); return $this->morphedByMany(\App\Models\Device::class, 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'alert_schedulable_id');
} }
public function deviceGroups() public function deviceGroups()
{ {
return $this->morphedByMany(\App\Models\DeviceGroup::class, 'alert_schedulable'); return $this->morphedByMany(\App\Models\DeviceGroup::class, 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'alert_schedulable_id');
}
public function locations()
{
return $this->morphedByMany(\App\Models\Location::class, 'alert_schedulable', 'alert_schedulables', 'schedule_id', 'alert_schedulable_id');
} }
} }

View File

@@ -133,25 +133,25 @@ class Device extends BaseModel
public function isUnderMaintenance() public function isUnderMaintenance()
{ {
if (!$this->device_id) {
return false;
}
$query = AlertSchedule::isActive() $query = AlertSchedule::isActive()
->join('alert_schedulables', 'alert_schedule.schedule_id', 'alert_schedulables.schedule_id') ->where(function (Builder $query) {
->where(function ($query) { $query->whereHas('devices', function (Builder $query) {
$query->where(function ($query) { $query->where('alert_schedulables.alert_schedulable_id', $this->device_id);
$query->where('alert_schedulable_type', 'device')
->where('alert_schedulable_id', $this->device_id);
}); });
if ($this->groups) { if ($this->groups) {
$query->orWhere(function ($query) { $query->orWhereHas('deviceGroups', function (Builder $query) {
$query->where('alert_schedulable_type', 'device_group') $query->whereIn('alert_schedulables.alert_schedulable_id', $this->groups->pluck('id'));
->whereIn('alert_schedulable_id', $this->groups->pluck('id'));
}); });
} }
if ($this->location) { if ($this->location) {
$query->orWhere(function ($query) { $query->orWhereHas('locations', function (Builder $query) {
$query->where('alert_schedulable_type', 'location') $query->where('alert_schedulables.alert_schedulable_id', $this->location->id);
->where('alert_schedulable_id', $this->location->id);
}); });
} }
}); });

View File

@@ -85,6 +85,7 @@ class AppServiceProvider extends ServiceProvider
'sensor' => \App\Models\Sensor::class, 'sensor' => \App\Models\Sensor::class,
'device' => \App\Models\Device::class, 'device' => \App\Models\Device::class,
'device_group' => \App\Models\DeviceGroup::class, 'device_group' => \App\Models\DeviceGroup::class,
'location' => \App\Models\Location::class,
], $sensor_types)); ], $sensor_types));
} }

View File

@@ -212,6 +212,7 @@ return [
'Config' => Illuminate\Support\Facades\Config::class, 'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class, 'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class, 'Event' => Illuminate\Support\Facades\Event::class,

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AlertScheduleUtc extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (\LibreNMS\DB\Eloquent::getDriver() == 'mysql') {
DB::table('alert_schedule')->update([
'start' => DB::raw("CONVERT_TZ(IF(`recurring` = 1, STR_TO_DATE(CONCAT(start_recurring_dt, ' ', start_recurring_hr), '%Y-%m-%d %H:%i:%s'), start), @@global.time_zone, '+00:00')"),
'end' => DB::raw("CONVERT_TZ(IF(`recurring` = 1, STR_TO_DATE(CONCAT(IFNULL(end_recurring_dt, '9000-09-09'), ' ', end_recurring_hr), '%Y-%m-%d %H:%i:%s'), end), @@global.time_zone, '+00:00')"),
'recurring_day' => DB::raw('REPLACE(recurring_day, 0, 7)'), // convert to RFC N date format
]);
}
Schema::table('alert_schedule', function (Blueprint $table) {
$table->dropColumn(['start_recurring_dt', 'start_recurring_hr', 'end_recurring_dt', 'end_recurring_hr']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('alert_schedule', function (Blueprint $table) {
$table->date('start_recurring_dt')->nullable(false)->default('1970-01-01')->after('end');
$table->time('start_recurring_hr')->nullable(false)->default('00:00:00')->after('start_recurring_dt');
$table->date('end_recurring_dt')->nullable()->after('start_recurring_hr');
$table->time('end_recurring_hr')->nullable(false)->default('00:00:00')->after('end_recurring_dt');
});
if (\LibreNMS\DB\Eloquent::getDriver() == 'mysql') {
DB::table('alert_schedule')->update([
'start' => DB::raw("CONVERT_TZ(start, '+00:00', @@global.time_zone)"),
'end' => DB::raw("CONVERT_TZ(end, '+00:00', @@global.time_zone)"),
'start_recurring_dt' => DB::raw("DATE(CONVERT_TZ(start, '+00:00', @@global.time_zone))"),
'start_recurring_hr' => DB::raw("TIME(CONVERT_TZ(start, '+00:00', @@global.time_zone))"),
'end_recurring_dt' => DB::raw("DATE(CONVERT_TZ(end, '+00:00', @@global.time_zone))"),
'end_recurring_hr' => DB::raw("TIME(CONVERT_TZ(end, '+00:00', @@global.time_zone))"),
'recurring_day' => DB::raw('REPLACE(recurring_day, 7, 0)'),
]);
}
}
}

View File

@@ -14,6 +14,8 @@ use Illuminate\Support\Str;
* the source code distribution for details. * the source code distribution for details.
*/ */
use Carbon\Carbon;
if (!Auth::user()->hasGlobalAdmin()) { if (!Auth::user()->hasGlobalAdmin()) {
header('Content-type: text/plain'); header('Content-type: text/plain');
die('ERROR: You need to be admin'); die('ERROR: You need to be admin');
@@ -34,7 +36,7 @@ if ($sub_type == 'new-maintenance') {
$title = mres($_POST['title']); $title = mres($_POST['title']);
$notes = mres($_POST['notes']); $notes = mres($_POST['notes']);
$recurring = mres($_POST['recurring']); $recurring = $_POST['recurring'] ? 1 : 0;
$start_recurring_dt = mres($_POST['start_recurring_dt']); $start_recurring_dt = mres($_POST['start_recurring_dt']);
$end_recurring_dt = mres($_POST['end_recurring_dt']); $end_recurring_dt = mres($_POST['end_recurring_dt']);
$start_recurring_hr = mres($_POST['start_recurring_hr']); $start_recurring_hr = mres($_POST['start_recurring_hr']);
@@ -75,7 +77,7 @@ if ($sub_type == 'new-maintenance') {
$message .= 'Please check end recurring date<br />'; $message .= 'Please check end recurring date<br />';
} }
} else { } else {
$end_recurring_dt = null; $end_recurring_dt = '9000-09-09';
} }
if (empty($start_recurring_hr)) { if (empty($start_recurring_hr)) {
@@ -117,18 +119,27 @@ if ($sub_type == 'new-maintenance') {
} }
if (empty($message)) { if (empty($message)) {
if (empty($schedule_id)) { $alert_schedule = \App\Models\AlertSchedule::findOrNew($schedule_id);
$schedule_id = dbInsert(array('recurring' => $recurring, 'start' => $start, 'end' => $end, 'start_recurring_dt' => $start_recurring_dt, 'end_recurring_dt' => $end_recurring_dt, 'start_recurring_hr' => $start_recurring_hr, 'end_recurring_hr' => $end_recurring_hr, 'recurring_day' => $recurring_day, 'title' => $title, 'notes' => $notes), 'alert_schedule'); $alert_schedule->title = $title;
} else { $alert_schedule->notes = $notes;
dbUpdate(array('recurring' => $recurring, 'start' => $start, 'end' => $end, 'start_recurring_dt' => $start_recurring_dt, 'end_recurring_dt' => $end_recurring_dt, 'start_recurring_hr' => $start_recurring_hr, 'end_recurring_hr' => $end_recurring_hr, 'recurring_day' => $recurring_day, 'title' => $title, 'notes' => $notes), 'alert_schedule', '`schedule_id`=?', array($schedule_id)); $alert_schedule->recurring = $recurring;
} $alert_schedule->start = $start;
$alert_schedule->end = $end;
if ($schedule_id > 0) { if ($recurring) {
$alert_schedule->start_recurring_dt = $start_recurring_dt;
$alert_schedule->start_recurring_hr = $start_recurring_hr;
$alert_schedule->end_recurring_dt = $end_recurring_dt;
$alert_schedule->end_recurring_hr = $end_recurring_hr;
}
$alert_schedule->save();
if ($alert_schedule->schedule_id > 0) {
$items = array(); $items = array();
$fail = 0; $fail = 0;
if ($update == 1) { if ($update == 1) {
dbDelete('alert_schedulables', '`schedule_id`=?', array($schedule_id)); dbDelete('alert_schedulables', '`schedule_id`=?', [$alert_schedule->schedule_id]);
} }
foreach ($_POST['maps'] as $target) { foreach ($_POST['maps'] as $target) {
@@ -141,7 +152,7 @@ if ($sub_type == 'new-maintenance') {
$target = substr($target, 1); $target = substr($target, 1);
} }
$item = dbInsert(['schedule_id' => $schedule_id, 'alert_schedulable_type' => $type, 'alert_schedulable_id' => $target], 'alert_schedulables'); $item = dbInsert(['schedule_id' => $alert_schedule->schedule_id, 'alert_schedulable_type' => $type, 'alert_schedulable_id' => $target], 'alert_schedulables');
if ($notes && $type = 'device' && get_user_pref('add_schedule_note_to_device', false)) { if ($notes && $type = 'device' && get_user_pref('add_schedule_note_to_device', false)) {
$device_notes = dbFetchCell('SELECT `notes` FROM `devices` WHERE `device_id` = ?;', [$target]); $device_notes = dbFetchCell('SELECT `notes` FROM `devices` WHERE `device_id` = ?;', [$target]);
$device_notes.= ((empty($device_notes)) ? '' : PHP_EOL) . date("Y-m-d H:i") . ' Alerts delayed: ' . $notes; $device_notes.= ((empty($device_notes)) ? '' : PHP_EOL) . date("Y-m-d H:i") . ' Alerts delayed: ' . $notes;
@@ -159,7 +170,7 @@ if ($sub_type == 'new-maintenance') {
dbDelete('alert_schedulables', '`item_id`=?', array($item)); dbDelete('alert_schedulables', '`item_id`=?', array($item));
} }
dbDelete('alert_schedule', '`schedule_id`=?', array($schedule_id)); dbDelete('alert_schedule', '`schedule_id`=?', [$alert_schedule->schedule_id]);
$message = 'Issue scheduling maintenance'; $message = 'Issue scheduling maintenance';
} else { } else {
$status = 'ok'; $status = 'ok';
@@ -176,7 +187,7 @@ if ($sub_type == 'new-maintenance') {
); );
} elseif ($sub_type == 'parse-maintenance') { } elseif ($sub_type == 'parse-maintenance') {
$schedule_id = mres($_POST['schedule_id']); $schedule_id = mres($_POST['schedule_id']);
$schedule = dbFetchRow('SELECT * FROM `alert_schedule` WHERE `schedule_id`=?', array($schedule_id)); $alert_schedule = \App\Models\AlertSchedule::findOrFail($schedule_id);
$items = []; $items = [];
foreach (dbFetchRows('SELECT `alert_schedulable_type`, `alert_schedulable_id` FROM `alert_schedulables` WHERE `schedule_id`=?', [$schedule_id]) as $target) { foreach (dbFetchRows('SELECT `alert_schedulable_type`, `alert_schedulable_id` FROM `alert_schedulables` WHERE `schedule_id`=?', [$schedule_id]) as $target) {
$id = $target['alert_schedulable_id']; $id = $target['alert_schedulable_id'];
@@ -195,19 +206,9 @@ if ($sub_type == 'new-maintenance') {
]; ];
} }
$response = array( $response = $alert_schedule->toArray();
'start' => $schedule['start'], $response['recurring_day'] = $alert_schedule->getOriginal('recurring_day');
'end' => $schedule['end'], $response['targets'] = $items;
'title' => $schedule['title'],
'notes' => $schedule['notes'],
'recurring' => $schedule['recurring'],
'start_recurring_dt' => ($schedule['start_recurring_dt'] != '0000-00-00' ? $schedule['start_recurring_dt']: '1970-01-02 00:00:01'),
'end_recurring_dt' => ($schedule['end_recurring_dt']!= '0000-00-00' ? $schedule['end_recurring_dt'] : '1970-01-02 00:00:01'),
'start_recurring_hr' => substr($schedule['start_recurring_hr'], 0, 5),
'end_recurring_hr' => substr($schedule['end_recurring_hr'], 0, 5),
'recurring_day' => $schedule['recurring_day'],
'targets' => $items,
);
} elseif ($sub_type == 'del-maintenance') { } elseif ($sub_type == 'del-maintenance') {
$schedule_id = mres($_POST['del_schedule_id']); $schedule_id = mres($_POST['del_schedule_id']);
dbDelete('alert_schedule', '`schedule_id`=?', array($schedule_id)); dbDelete('alert_schedule', '`schedule_id`=?', array($schedule_id));

View File

@@ -93,13 +93,13 @@ if (\Auth::user()->hasGlobalAdmin()) {
<div class="form-group"> <div class="form-group">
<label for="recurring_day" class="col-sm-4 control-label">Only on weekday: </label> <label for="recurring_day" class="col-sm-4 control-label">Only on weekday: </label>
<div class="col-sm-8"> <div class="col-sm-8">
<div style="float: left;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="1" />Mo</label></div> <div style="float: left;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="1" />Mo</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="2" />Tu</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="2" />Tu</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="3" />We</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="3" />We</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="4" />Th</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="4" />Th</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="5" />Fr</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="5" />Fr</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="6" />Sa</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="6" />Sa</label></div>
<div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" id="recurring_day" name="recurring_day[]" value="0" />Su</label></div> <div style="float: left;padding-left: 20px;"><label><input type="checkbox" style="width: 20px;" class="form-control" name="recurring_day[]" value="7" />Su</label></div>
</div> </div>
</div> </div>
</div> </div>
@@ -126,7 +126,6 @@ $('#schedule-maintenance').on('hide.bs.modal', function (event) {
$('#schedule_id').val(''); $('#schedule_id').val('');
$('#title').val(''); $('#title').val('');
$('#notes').val(''); $('#notes').val('');
$('#recurring').val('');
$('#start').val(moment().format('YYYY-MM-DD HH:mm')).data("DateTimePicker").maxDate(false).minDate(moment()); $('#start').val(moment().format('YYYY-MM-DD HH:mm')).data("DateTimePicker").maxDate(false).minDate(moment());
$('#end').val(moment().add(1, 'hour').format('YYYY-MM-DD HH:mm')).data("DateTimePicker").maxDate(false).minDate(moment()); $('#end').val(moment().add(1, 'hour').format('YYYY-MM-DD HH:mm')).data("DateTimePicker").maxDate(false).minDate(moment());
var $startRecurringDt = $('#start_recurring_dt'); var $startRecurringDt = $('#start_recurring_dt');
@@ -138,7 +137,7 @@ $('#schedule-maintenance').on('hide.bs.modal', function (event) {
$('#start_recurring_hr').val('').data("DateTimePicker").minDate(false).maxDate(false); $('#start_recurring_hr').val('').data("DateTimePicker").minDate(false).maxDate(false);
$('#end_recurring_hr').val('').data("DateTimePicker").minDate(false).maxDate(false); $('#end_recurring_hr').val('').data("DateTimePicker").minDate(false).maxDate(false);
$('#recurring_day').prop('checked', false); $("input[name='recurring_day[]']").prop('checked', false);
$("#recurring").bootstrapSwitch('state', false); $("#recurring").bootstrapSwitch('state', false);
$('#recurring').val(0); $('#recurring').val(0);
$('#norecurringgroup').show(); $('#norecurringgroup').show();
@@ -184,7 +183,7 @@ $('#schedule-maintenance').on('show.bs.modal', function (event) {
$('#end_recurring_dt').val(''); $('#end_recurring_dt').val('');
$('#start_recurring_hr').val(''); $('#start_recurring_hr').val('');
$('#end_recurring_hr').val(''); $('#end_recurring_hr').val('');
$('#recurring_day').prop('checked', false); $("input[name='recurring_day[]']").prop('checked', false);
$("#recurring").bootstrapSwitch('state', false); $("#recurring").bootstrapSwitch('state', false);
$('#recurring').val(0); $('#recurring').val(0);
}else{ }else{
@@ -211,7 +210,7 @@ $('#schedule-maintenance').on('show.bs.modal', function (event) {
$("input[name='recurring_day[]'][value="+checkedday+"]").prop('checked', true); $("input[name='recurring_day[]'][value="+checkedday+"]").prop('checked', true);
}); });
}else{ }else{
$('#recurring_day').prop('checked', false); $("input[name='recurring_day[]']").prop('checked', false);
} }
$('#norecurringgroup').hide(); $('#norecurringgroup').hide();

View File

@@ -31,8 +31,8 @@ if (Auth::user()->hasGlobalAdmin()) {
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger danger" id="sched-maintenance-removal" data-target="sched-maintenance-removal">Delete</button> <button type="submit" class="btn btn-danger danger" id="sched-maintenance-removal" data-target="sched-maintenance-removal">Delete</button>
<input type="hidden" name="del_schedule_id" id="del_schedule_id"> <input type="hidden" name="del_schedule_id" id="del_schedule_id">
<input type="hidden" name="type" id="type" value="schedule-maintenance"> <input type="hidden" name="type" value="schedule-maintenance">
<input type="hidden" name="sub_type" id="sub_type" value="del-maintenance"> <input type="hidden" name="sub_type" value="del-maintenance">
</form> </form>
</div> </div>
</div> </div>

View File

@@ -77,12 +77,7 @@ var grid = $("#alert-schedule").bootgrid({
"<div class=\"col-sm-4 actionBar\"><p class=\"{{css.search}}\"></p><p class=\"{{css.actions}}\"></p></div></div></div>" "<div class=\"col-sm-4 actionBar\"><p class=\"{{css.search}}\"></p><p class=\"{{css.actions}}\"></p></div></div></div>"
}, },
rowCount: [50, 100, 250, -1], rowCount: [50, 100, 250, -1],
post: function () { url: "ajax/table/alert-schedule"
return {
id: "alert-schedule",
};
},
url: "ajax_table.php"
}).on("loaded.rs.jquery.bootgrid", function() { }).on("loaded.rs.jquery.bootgrid", function() {
/* Executes after data is loaded and rendered */ /* Executes after data is loaded and rendered */
grid.find(".command-edit").on("click", function(e) { grid.find(".command-edit").on("click", function(e) {

View File

@@ -307,7 +307,8 @@ If `devices.ignore = 0` or `macros.device = 1` condition is is set and ignore al
<script> <script>
$('[type="checkbox"]').bootstrapSwitch('offColor', 'danger'); $('[type="checkbox"]').bootstrapSwitch('offColor', 'danger');
$("#maintenance").click(function() { $("#maintenance").click(function(event) {
event.preventDefault();
var device_id = $(this).data("device_id"); var device_id = $(this).data("device_id");
var title = '<?=display($device['hostname']);?>'; var title = '<?=display($device['hostname']);?>';
var notes = ''; var notes = '';

View File

@@ -1,133 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version. Please see LICENSE.txt at the top level of
* the source code distribution for details.
*/
$where = 1;
$sql = " FROM `alert_schedule` AS S WHERE $where";
if (!Auth::user()->hasGlobalRead()) {
$param[] = Auth::id();
}
if (isset($searchPhrase) && !empty($searchPhrase)) {
$sql .= " AND (`S`.`title` LIKE '%$searchPhrase%' OR `S`.`start` LIKE '%$searchPhrase%' OR `S`.`end` LIKE '%$searchPhrase%')";
}
$count_sql = "SELECT COUNT(`schedule_id`) $sql";
$total = dbFetchCell($count_sql, $param);
if (empty($total)) {
$total = 0;
}
if (isset($sort) && !empty($sort)) {
list($sort_column, $sort_order) = explode(' ', trim($sort));
if ($sort_column == 'status') {
$sort_by_status = true;
$sort = "`S`.`start` $sort_order";
}
} else {
$sort = '`S`.`start` DESC ';
}
$sql .= " ORDER BY $sort";
if (isset($current)) {
$limit_low = (($current * $rowCount) - ($rowCount));
$limit_high = $rowCount;
}
if ($rowCount != -1) {
$sql .= " LIMIT $limit_low,$limit_high";
}
$sql = "SELECT `S`.`schedule_id`, `S`.`recurring`, DATE_FORMAT(NOW(), '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') AS now, DATE_FORMAT(`S`.`start`, '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') AS `start`, DATE_FORMAT(`S`.`end`, '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') AS `end`, DATE_FORMAT(`S`.`start_recurring_dt`, '" . \LibreNMS\Config::get('dateformat.mysql.date') . "') AS `start_recurring_dt`, DATE_FORMAT(`S`.`end_recurring_dt`, '" . \LibreNMS\Config::get('dateformat.mysql.date') . "') AS `end_recurring_dt`, `S`.`start_recurring_hr`, `S`.`end_recurring_hr`, `S`.`recurring_day`, `S`.`title` $sql";
foreach (dbFetchRows($sql, $param) as $schedule) {
$status = 0;
if ($schedule['recurring'] == 0) {
$start = strtotime($schedule['start']);
$end = strtotime($schedule['end']);
$now = strtotime($schedule['now']);
if ($end < $now) {
$status = 1;
}
if ($now >= $start && $now < $end) {
$status = 2;
}
} else {
$start = strtotime($schedule['start_recurring_dt']);
$end = $schedule['end_recurring_dt'] != '' && $schedule['end_recurring_dt'] != '0000-00-00' ? strtotime($schedule['end_recurring_dt'].' '. $schedule['end_recurring_hr']) : 0;
$now = strtotime($schedule['now']);
if ($end < $now && $end != 0) {
$status =1;
}
if ($start <= $now && ($now <= $end || $end == 0)) {
$status = 2;
}
}
$table_rd = '';
if ($schedule['recurring_day'] != '') {
$array_days = array(
0 => 'Su',
1 => 'Mo',
2 => 'Tu',
3 => 'We',
4 => 'Th',
5 => 'Fr',
6 => 'Sa'
);
$array_rd = explode(',', $schedule['recurring_day']);
foreach ($array_rd as $key_rd => $val_rd) {
if (array_key_exists($val_rd, $array_days)) {
$table_rd .= $table_rd != '' ? ','. $array_days[$val_rd] : $array_days[$val_rd];
}
}
}
$response[] = array(
'title' => $schedule['title'],
'recurring' => $schedule['recurring'] == 1 ? 'yes' : 'no',
'start' => $schedule['recurring'] == 1 ? '' : $schedule['start'],
'end' => $schedule['recurring'] == 1 ? '' : $schedule['end'],
'start_recurring_dt' => $schedule['recurring'] == 0 || $schedule['start_recurring_dt'] == '0000-00-00' ? '' : $schedule['start_recurring_dt'],
'end_recurring_dt' => $schedule['recurring'] == 0 || $schedule['end_recurring_dt'] == '0000-00-00' ? '' : $schedule['end_recurring_dt'],
'start_recurring_hr' => $schedule['recurring'] == 0 ? '' : substr($schedule['start_recurring_hr'], 0, 5),
'end_recurring_hr' => $schedule['recurring'] == 0 ? '' : substr($schedule['end_recurring_hr'], 0, 5),
'recurring_day' => $schedule['recurring'] == 0 ? '' : $table_rd,
'id' => $schedule['schedule_id'],
'status' => $status,
);
}
if (isset($sort_by_status) && $sort_by_status) {
if ($sort_order == 'asc') {
usort($response, function ($a, $b) {
return $a['status'] - $b['status'];
});
} else {
usort($response, function ($a, $b) {
return $b['status'] - $a['status'];
});
}
}
$output = array(
'current' => $current,
'rowCount' => $rowCount,
'rows' => $response,
'total' => $total,
);
echo _json_encode($output);

View File

@@ -103,10 +103,6 @@ alert_schedule:
- { Field: recurring, Type: 'tinyint unsigned', 'Null': false, Extra: '', Default: '0' } - { Field: recurring, Type: 'tinyint unsigned', 'Null': false, Extra: '', Default: '0' }
- { Field: start, Type: datetime, 'Null': false, Extra: '', Default: '1970-01-02 00:00:01' } - { Field: start, Type: datetime, 'Null': false, Extra: '', Default: '1970-01-02 00:00:01' }
- { Field: end, Type: datetime, 'Null': false, Extra: '', Default: '1970-01-02 00:00:01' } - { Field: end, Type: datetime, 'Null': false, Extra: '', Default: '1970-01-02 00:00:01' }
- { Field: start_recurring_dt, Type: date, 'Null': false, Extra: '', Default: '1970-01-01' }
- { Field: end_recurring_dt, Type: date, 'Null': true, Extra: '' }
- { Field: start_recurring_hr, Type: time, 'Null': false, Extra: '', Default: '00:00:00' }
- { Field: end_recurring_hr, Type: time, 'Null': false, Extra: '', Default: '00:00:00' }
- { Field: recurring_day, Type: varchar(15), 'Null': true, Extra: '' } - { Field: recurring_day, Type: varchar(15), 'Null': true, Extra: '' }
- { Field: title, Type: varchar(255), 'Null': false, Extra: '' } - { Field: title, Type: varchar(255), 'Null': false, Extra: '' }
- { Field: notes, Type: text, 'Null': false, Extra: '' } - { Field: notes, Type: text, 'Null': false, Extra: '' }

View File

@@ -5,7 +5,7 @@
@if($parent_id) @if($parent_id)
<a href="{{ route('device', $parent_id) }}" title="@lang('VM Host')"><i class="fa fa-server fa-fw fa-lg"></i></a> <a href="{{ route('device', $parent_id) }}" title="@lang('VM Host')"><i class="fa fa-server fa-fw fa-lg"></i></a>
@endif @endif
@if(\LibreNMS\Alert\AlertUtil::isMaintenance($device_id)) @if($device->isUnderMaintenance())
<span title="@lang('Scheduled Maintenance')" class="fa fa-wrench fa-fw fa-lg"></span> <span title="@lang('Scheduled Maintenance')" class="fa fa-wrench fa-fw fa-lg"></span>
@endif @endif
<span style="font-size: 20px;">@deviceLink($device)</span><br/> <span style="font-size: 20px;">@deviceLink($device)</span><br/>

View File

@@ -108,6 +108,7 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
// jquery bootgrid data controllers // jquery bootgrid data controllers
Route::group(['prefix' => 'table', 'namespace' => 'Table'], function () { Route::group(['prefix' => 'table', 'namespace' => 'Table'], function () {
Route::post('alert-schedule', 'AlertScheduleController');
Route::post('customers', 'CustomersController'); Route::post('customers', 'CustomersController');
Route::post('device', 'DeviceController'); Route::post('device', 'DeviceController');
Route::post('eventlog', 'EventlogController'); Route::post('eventlog', 'EventlogController');