master
go 122 lines 3.39 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package l2topology
4
5 import (
6 "context"
7 "fmt"
8 "time"
9 )
10
11 // ObservationProvider gathers normalized L2 observations for discovery requests.
12 type ObservationProvider interface {
13 ObserveByCIDRs(ctx context.Context, req CIDRRequest) ([]L2Observation, error)
14 ObserveByDevices(ctx context.Context, req DeviceRequest) ([]L2Observation, error)
15 }
16
17 // RuntimeEngine executes discovery using a concrete observation provider.
18 type RuntimeEngine struct {
19 provider ObservationProvider
20 }
21
22 // NewRuntimeEngine constructs a concrete engine backed by the given provider.
23 func NewRuntimeEngine(provider ObservationProvider) (*RuntimeEngine, error) {
24 if provider == nil {
25 return nil, fmt.Errorf("%w: observation provider is required", ErrInvalidRequest)
26 }
27 return &RuntimeEngine{provider: provider}, nil
28 }
29
30 func (e *RuntimeEngine) DiscoverByCIDRs(ctx context.Context, req CIDRRequest) (Result, error) {
31 if e == nil || e.provider == nil {
32 return Result{}, fmt.Errorf("%w: observation provider is not configured", ErrInvalidRequest)
33 }
34 if err := validateCIDRRequest(req); err != nil {
35 return Result{}, err
36 }
37
38 req.Options = ensureCollectedAt(req.Options)
39
40 observations, err := e.provider.ObserveByCIDRs(ctx, req)
41 if err != nil {
42 return Result{}, fmt.Errorf("observe cidr discovery request: %w", err)
43 }
44 if len(observations) == 0 {
45 return emptyResult(req.Options.CollectedAt), nil
46 }
47
48 result, err := BuildL2ResultFromObservations(observations, req.Options)
49 if err != nil {
50 return Result{}, fmt.Errorf("build l2 result from cidr discovery request: %w", err)
51 }
52 return result, nil
53 }
54
55 func (e *RuntimeEngine) DiscoverByDevices(ctx context.Context, req DeviceRequest) (Result, error) {
56 if e == nil || e.provider == nil {
57 return Result{}, fmt.Errorf("%w: observation provider is not configured", ErrInvalidRequest)
58 }
59 if err := validateDeviceRequest(req); err != nil {
60 return Result{}, err
61 }
62
63 req.Options = ensureCollectedAt(req.Options)
64
65 observations, err := e.provider.ObserveByDevices(ctx, req)
66 if err != nil {
67 return Result{}, fmt.Errorf("observe device discovery request: %w", err)
68 }
69 if len(observations) == 0 {
70 return emptyResult(req.Options.CollectedAt), nil
71 }
72
73 result, err := BuildL2ResultFromObservations(observations, req.Options)
74 if err != nil {
75 return Result{}, fmt.Errorf("build l2 result from device discovery request: %w", err)
76 }
77 return result, nil
78 }
79
80 func validateCIDRRequest(req CIDRRequest) error {
81 if len(req.CIDRs) == 0 {
82 return fmt.Errorf("%w: cidrs are required", ErrInvalidRequest)
83 }
84 for i := range req.CIDRs {
85 if !req.CIDRs[i].IsValid() {
86 return fmt.Errorf("%w: cidrs[%d] has invalid prefix", ErrInvalidRequest, i)
87 }
88 }
89 return nil
90 }
91
92 func validateDeviceRequest(req DeviceRequest) error {
93 if len(req.Devices) == 0 {
94 return fmt.Errorf("%w: devices are required", ErrInvalidRequest)
95 }
96 for i := range req.Devices {
97 if !req.Devices[i].Address.IsValid() {
98 return fmt.Errorf("%w: devices[%d] has invalid address", ErrInvalidRequest, i)
99 }
100 }
101 return nil
102 }
103
104 func ensureCollectedAt(opts DiscoverOptions) DiscoverOptions {
105 if opts.CollectedAt.IsZero() {
106 opts.CollectedAt = time.Now().UTC()
107 } else {
108 opts.CollectedAt = opts.CollectedAt.UTC()
109 }
110 return opts
111 }
112
113 func emptyResult(collectedAt time.Time) Result {
114 if !collectedAt.IsZero() {
115 collectedAt = collectedAt.UTC()
116 }
117
118 return Result{
119 CollectedAt: collectedAt,
120 Stats: newL2ResultStats(),
121 }
122 }