Skip to content

Commit 518e709

Browse files
committed
Add Operation(Segments) to the API
1 parent 17db7e2 commit 518e709

8 files changed

Lines changed: 583 additions & 47 deletions

File tree

app/Http/Controllers/AlertOperationController.php

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
use App\Http\Requests\AlertOperationRequest;
66
use App\Models\AlertOperation;
7+
use App\Services\Alerting\AlertOperationSyncer;
78
use Illuminate\Contracts\View\View;
89
use Illuminate\Http\JsonResponse;
910
use Illuminate\Support\Facades\Gate;
10-
use Illuminate\Support\Str;
11-
use LibreNMS\Enum\AlertRuleOperationPhase;
1211

1312
class AlertOperationController extends Controller
1413
{
@@ -59,7 +58,7 @@ public function store(AlertOperationRequest $request): JsonResponse
5958
$op->default_operation_step_duration_seconds = $validated['default_operation_step_duration_seconds'] ?? null;
6059
$op->notifications_suppressed = (bool) ($validated['notifications_suppressed'] ?? false);
6160
$op->save();
62-
$this->syncSegments($op, $request);
61+
AlertOperationSyncer::sync($op, $request->validated('segments') ?? []);
6362

6463
return response()->json([
6564
'status' => 'ok',
@@ -84,7 +83,7 @@ public function update(AlertOperationRequest $request, AlertOperation $alertOper
8483
$alertOperation->default_operation_step_duration_seconds = $validated['default_operation_step_duration_seconds'] ?? null;
8584
$alertOperation->notifications_suppressed = (bool) ($validated['notifications_suppressed'] ?? false);
8685
$alertOperation->save();
87-
$this->syncSegments($alertOperation, $request);
86+
AlertOperationSyncer::sync($alertOperation, $request->validated('segments') ?? []);
8887

8988
return response()->json([
9089
'status' => 'ok',
@@ -113,47 +112,4 @@ public function destroy(AlertOperation $alertOperation): JsonResponse
113112

114113
return response()->json(['status' => 'ok', 'message' => __('Operation deleted')]);
115114
}
116-
117-
private function syncSegments(AlertOperation $op, AlertOperationRequest $request): void
118-
{
119-
$rows = $request->validated('segments');
120-
$op->segments()->delete();
121-
122-
foreach (array_values($rows) as $idx => $row) {
123-
$from = max(1, (int) ($row['escalation_step_from'] ?? 1));
124-
$toRaw = $row['escalation_step_to'] ?? null;
125-
$to = ($toRaw === '' || $toRaw === null) ? null : max($from, (int) $toRaw);
126-
127-
$seg = $op->segments()->create([
128-
'position' => (int) ($row['position'] ?? $idx),
129-
'operation_phase' => AlertRuleOperationPhase::PROBLEM,
130-
'escalation_step_from' => $from,
131-
'escalation_step_to' => $to,
132-
'start_in_seconds' => max(0, (int) ($row['start_in_seconds'] ?? 0)),
133-
'step_duration_seconds' => max(0, (int) ($row['step_duration_seconds'] ?? 0)),
134-
]);
135-
136-
$transportsRaw = $row['transports'] ?? [];
137-
if (! is_array($transportsRaw)) {
138-
$transportsRaw = [];
139-
}
140-
$transportsRaw = array_values(array_filter($transportsRaw, fn ($t) => $t !== null && $t !== ''));
141-
if ($transportsRaw === []) {
142-
throw new \InvalidArgumentException('Each segment must have at least one transport or transport group.');
143-
}
144-
145-
$transportIds = [];
146-
$transportGroupIds = [];
147-
foreach ($transportsRaw as $transport) {
148-
if (Str::startsWith((string) $transport, 'g')) {
149-
$transportGroupIds[] = (int) substr((string) $transport, 1);
150-
} else {
151-
$transportIds[] = (int) $transport;
152-
}
153-
}
154-
155-
$seg->transportSingles()->syncWithPivotValues($transportIds, ['target_type' => 'single']);
156-
$seg->transportGroups()->syncWithPivotValues($transportGroupIds, ['target_type' => 'group']);
157-
}
158-
}
159115
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Policies;
4+
5+
use App\Models\AlertOperationSegment;
6+
use App\Models\User;
7+
8+
/**
9+
* Segments are managed through their parent {@see \App\Models\AlertOperation}.
10+
* Authorization mirrors the parent: the v1 API exposes segments as a read-only
11+
* sub-resource so {@see \App\Restify\AlertOperationSegmentRepository} only needs
12+
* view/viewAny here. Writes happen via the operation, gated by AlertOperationPolicy.
13+
*/
14+
class AlertOperationSegmentPolicy
15+
{
16+
use ChecksGlobalPermissions;
17+
18+
public function __construct()
19+
{
20+
$this->globalPrefix = 'alert-operation';
21+
}
22+
23+
public function viewAny(User $user): bool
24+
{
25+
return $this->hasGlobalPermission($user, 'view')
26+
|| $this->hasGlobalPermission($user, 'viewAll')
27+
|| $this->hasGlobalPermission($user, 'create')
28+
|| $this->hasGlobalPermission($user, 'update')
29+
|| $this->hasGlobalPermission($user, 'delete');
30+
}
31+
32+
public function viewAll(User $user): bool
33+
{
34+
return $this->hasGlobalPermission($user, 'viewAll');
35+
}
36+
37+
public function view(User $user, AlertOperationSegment $segment): bool
38+
{
39+
if ($this->hasGlobalPermission($user, 'viewAll')) {
40+
return true;
41+
}
42+
43+
return $this->hasGlobalPermission($user, 'view');
44+
}
45+
}

app/Providers/RestifyServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use App\Restify\BgpPeerRepository;
88
use App\Restify\BillRepository;
99
use App\Restify\AlertLogRepository;
10+
use App\Restify\AlertOperationRepository;
11+
use App\Restify\AlertOperationSegmentRepository;
1012
use App\Restify\AlertRepository;
1113
use App\Restify\AlertRuleRepository;
1214
use App\Restify\AlertScheduleRepository;
@@ -125,6 +127,8 @@ public function boot(): void
125127

126128
Restify::repositories([
127129
AlertLogRepository::class,
130+
AlertOperationRepository::class,
131+
AlertOperationSegmentRepository::class,
128132
AlertRepository::class,
129133
AlertRuleRepository::class,
130134
AlertScheduleRepository::class,
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
namespace App\Restify;
4+
5+
use App\Http\Requests\AlertOperationRequest;
6+
use App\Models\AlertOperation;
7+
use App\Services\Alerting\AlertOperationSyncer;
8+
use Binaryk\LaravelRestify\Fields\HasMany;
9+
use Binaryk\LaravelRestify\Filters\MatchFilter;
10+
use Binaryk\LaravelRestify\Filters\SearchableFilter;
11+
use Binaryk\LaravelRestify\Filters\SortableFilter;
12+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
13+
use Illuminate\Support\Facades\Validator;
14+
use Illuminate\Validation\ValidationException;
15+
16+
class AlertOperationRepository extends Repository
17+
{
18+
public static string $model = AlertOperation::class;
19+
20+
public static string $title = 'name';
21+
22+
public static function related(): array
23+
{
24+
return [
25+
'segments' => HasMany::make('segments', AlertOperationSegmentRepository::class),
26+
];
27+
}
28+
29+
public static function searchables(): array
30+
{
31+
return [
32+
'name' => SearchableFilter::make()->setColumn('name'),
33+
];
34+
}
35+
36+
public static function matches(): array
37+
{
38+
return [
39+
'name' => MatchFilter::make()->setType('text')->setColumn('name'),
40+
'notificationsSuppressed' => MatchFilter::make()->setType('bool')->setColumn('notifications_suppressed'),
41+
'defaultOperationStepDurationSeconds' => MatchFilter::make()->setType('integer')->setColumn('default_operation_step_duration_seconds'),
42+
];
43+
}
44+
45+
public static function sorts(): array
46+
{
47+
return [
48+
'name' => SortableFilter::make()->setColumn('name'),
49+
'notificationsSuppressed' => SortableFilter::make()->setColumn('notifications_suppressed'),
50+
'defaultOperationStepDurationSeconds' => SortableFilter::make()->setColumn('default_operation_step_duration_seconds'),
51+
];
52+
}
53+
54+
public function fields(RestifyRequest $request): array
55+
{
56+
return [
57+
field('name')->rules('required', 'string', 'max:255'),
58+
field('defaultOperationStepDurationSeconds', fn ($value, $model) => $model->default_operation_step_duration_seconds)
59+
->fillCallback(function ($request, $model, $attribute) {
60+
if ($request->exists($attribute)) {
61+
$model->default_operation_step_duration_seconds = $request->input($attribute);
62+
}
63+
})
64+
->rules('nullable', 'integer', 'min:0'),
65+
field('notificationsSuppressed', fn ($value, $model) => (bool) $model->notifications_suppressed)
66+
->fillCallback(function ($request, $model, $attribute) {
67+
if ($request->exists($attribute)) {
68+
$model->notifications_suppressed = $request->boolean($attribute);
69+
}
70+
})
71+
->rules('boolean'),
72+
];
73+
}
74+
75+
public function store(RestifyRequest $request)
76+
{
77+
$segments = $this->validatedSegments($request);
78+
79+
$response = parent::store($request);
80+
81+
try {
82+
/** @var AlertOperation $operation */
83+
$operation = $this->resource;
84+
AlertOperationSyncer::sync($operation, $segments);
85+
} catch (\InvalidArgumentException $e) {
86+
throw ValidationException::withMessages(['segments' => $e->getMessage()]);
87+
}
88+
89+
return $response;
90+
}
91+
92+
public function update(RestifyRequest $request, $repositoryId)
93+
{
94+
$segments = $this->validatedSegments($request);
95+
96+
$response = parent::update($request, $repositoryId);
97+
98+
try {
99+
/** @var AlertOperation $operation */
100+
$operation = $this->resource;
101+
AlertOperationSyncer::sync($operation, $segments);
102+
} catch (\InvalidArgumentException $e) {
103+
throw ValidationException::withMessages(['segments' => $e->getMessage()]);
104+
}
105+
106+
return $response;
107+
}
108+
109+
public function destroy(RestifyRequest $request, $repositoryId)
110+
{
111+
/** @var AlertOperation $operation */
112+
$operation = $this->resource;
113+
if ($operation->alertRules()->exists()) {
114+
throw ValidationException::withMessages([
115+
'id' => 'This operation is assigned to one or more alert rules and cannot be deleted.',
116+
]);
117+
}
118+
119+
return parent::destroy($request, $repositoryId);
120+
}
121+
122+
/**
123+
* Run the segment portion of {@see AlertOperationRequest} rules and return the rows.
124+
*
125+
* @return array<int, array<string, mixed>>
126+
*/
127+
private function validatedSegments(RestifyRequest $request): array
128+
{
129+
$rules = (new AlertOperationRequest())->rules();
130+
$segmentRules = array_filter(
131+
$rules,
132+
fn (string $key) => $key === 'segments' || str_starts_with($key, 'segments.'),
133+
ARRAY_FILTER_USE_KEY,
134+
);
135+
136+
$payload = ['segments' => $request->input('segments', [])];
137+
if (is_array($payload['segments'])) {
138+
foreach ($payload['segments'] as $i => $seg) {
139+
if (is_array($seg) && ($seg['escalation_step_to'] ?? null) === '') {
140+
$payload['segments'][$i]['escalation_step_to'] = null;
141+
}
142+
}
143+
}
144+
145+
Validator::make($payload, $segmentRules)->validate();
146+
147+
return $payload['segments'];
148+
}
149+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace App\Restify;
4+
5+
use App\Models\AlertOperationSegment;
6+
use Binaryk\LaravelRestify\Fields\BelongsTo;
7+
use Binaryk\LaravelRestify\Filters\MatchFilter;
8+
use Binaryk\LaravelRestify\Filters\SortableFilter;
9+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
10+
use Illuminate\Http\Request;
11+
12+
class AlertOperationSegmentRepository extends Repository
13+
{
14+
public static string $model = AlertOperationSegment::class;
15+
16+
public static string $title = 'id';
17+
18+
public static function related(): array
19+
{
20+
return [
21+
'alertOperation' => BelongsTo::make('alertOperation', AlertOperationRepository::class),
22+
];
23+
}
24+
25+
public static function matches(): array
26+
{
27+
return [
28+
'alertOperationId' => MatchFilter::make()->setType('integer')->setColumn('alert_operation_id'),
29+
'operationPhase' => MatchFilter::make()->setType('text')->setColumn('operation_phase'),
30+
'escalationStepFrom' => MatchFilter::make()->setType('integer')->setColumn('escalation_step_from'),
31+
'escalationStepTo' => MatchFilter::make()->setType('integer')->setColumn('escalation_step_to'),
32+
];
33+
}
34+
35+
public static function sorts(): array
36+
{
37+
return [
38+
'position' => SortableFilter::make()->setColumn('position'),
39+
'alertOperationId' => SortableFilter::make()->setColumn('alert_operation_id'),
40+
];
41+
}
42+
43+
public function fields(RestifyRequest $request): array
44+
{
45+
return [
46+
field('alertOperationId', fn ($value, $model) => $model->alert_operation_id)->readonly(),
47+
field('position')->readonly(),
48+
field('operationPhase', fn ($value, $model) => $model->operation_phase)->readonly(),
49+
field('escalationStepFrom', fn ($value, $model) => $model->escalation_step_from)->readonly(),
50+
field('escalationStepTo', fn ($value, $model) => $model->escalation_step_to)->readonly(),
51+
field('startInSeconds', fn ($value, $model) => $model->start_in_seconds)->readonly(),
52+
field('stepDurationSeconds', fn ($value, $model) => $model->step_duration_seconds)->readonly(),
53+
field('transports', function ($value, $model) {
54+
if ($model instanceof AlertOperationSegment) {
55+
return $model->toApiArray()['transports'];
56+
}
57+
58+
return [];
59+
})->readonly(),
60+
];
61+
}
62+
63+
/**
64+
* Segments are created/updated as part of their parent AlertOperation.
65+
*/
66+
public static function authorizedToStore(Request $request): bool
67+
{
68+
return false;
69+
}
70+
71+
public function authorizedToUpdate(Request $request): bool
72+
{
73+
return false;
74+
}
75+
76+
public function authorizedToDelete(Request $request): bool
77+
{
78+
return false;
79+
}
80+
}

0 commit comments

Comments
 (0)