1 package host
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/url"
8 "os"
9
10 "github.com/gin-gonic/gin"
11 "github.com/gin-gonic/gin/binding"
12 "github.com/go-playground/validator/v10"
13 "github.com/spf13/afero"
14 v1 "k8s.io/api/core/v1"
15 toolscache "k8s.io/client-go/tools/cache"
16 "sigs.k8s.io/controller-runtime/pkg/client"
17
18 "edge-infra.dev/pkg/lib/logging"
19 "edge-infra.dev/pkg/sds/interlock/internal/config"
20 "edge-infra.dev/pkg/sds/interlock/internal/constants"
21 "edge-infra.dev/pkg/sds/interlock/topic"
22 "edge-infra.dev/pkg/sds/interlock/topic/host/internal/middleware"
23 "edge-infra.dev/pkg/sds/interlock/websocket"
24 )
25
26 var (
27
28 TopicName = "host"
29
30 Path = "/v1/host"
31 )
32
33
34 type Host struct {
35 topic topic.Topic
36 }
37
38
39
40 func New(ctx context.Context, cfg *config.Config, wm *websocket.Manager) (*Host, error) {
41 state, err := newState(ctx, cfg)
42 if err != nil {
43 return nil, err
44 }
45 topic := topic.NewTopic(
46 TopicName,
47 state,
48 cfg,
49 wm,
50 )
51
52 if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
53 if err := v.RegisterValidation("is_zero_xor_is_connected", validateFieldIsZeroXORStatusIsConnected); err != nil {
54 return nil, err
55 }
56 } else {
57 return nil, errors.New("could not register custom binding validation: binding validator is not of type validator.Validate")
58 }
59
60 host := &Host{
61 topic: topic,
62 }
63 err = host.SetupAPIInformers(ctx, cfg)
64 return host, err
65 }
66
67
68 func (h *Host) RegisterEndpoints(r *gin.Engine) {
69 v1 := r.Group(Path)
70 v1.GET("", h.topic.DefaultGet)
71 v1.PATCH("", middleware.ReadOnlyFieldValidation(), middleware.BlockVNC(), h.topic.DefaultPatch)
72 v1.POST("/vnc", h.VNCPost)
73 v1.PUT("/vnc", h.VNCPut)
74 v1.POST("/kpower", h.KpowerPost)
75 v1.PUT("/kpower", h.KpowerPut)
76 }
77
78
79
80
81 func (h *Host) SetupAPIInformers(ctx context.Context, cfg *config.Config) error {
82
83 informer, err := cfg.Cache.GetInformer(ctx, &v1.Node{})
84 if err != nil {
85 return fmt.Errorf("failed to get node resource informer: %w", err)
86 }
87
88 nodeFilteredEventHandler := toolscache.FilteringResourceEventHandler{
89 FilterFunc: IsHostNode,
90 Handler: &NodeUIDEventHandler{h.topic.UpdateState},
91 }
92
93 _, err = informer.AddEventHandler(nodeFilteredEventHandler)
94 if err != nil {
95 return fmt.Errorf("failed to add node event handler: %w", err)
96 }
97 return nil
98 }
99
100
101
102 func IsHostNode(obj interface{}) bool {
103 node, ok := obj.(*v1.Node)
104 if !ok {
105 return false
106 }
107 return node.GetName() == os.Getenv("NODE_NAME")
108 }
109
110
111
112 type NodeUIDEventHandler struct {
113 UpdateFunc func(topic.UpdateFunc) error
114 }
115
116
117 func (nueh *NodeUIDEventHandler) OnAdd(obj interface{}, _ bool) {
118 node := obj.(*v1.Node)
119 updateNodeUID(nueh.UpdateFunc, node)
120 }
121
122
123 func (nueh *NodeUIDEventHandler) OnUpdate(_, _ interface{}) {
124 }
125
126
127 func (nueh *NodeUIDEventHandler) OnDelete(_ interface{}) {}
128
129
130
131 func updateNodeUID(updateFunc func(topic.UpdateFunc) error, node *v1.Node) {
132 nodeUID := string(node.GetObjectMeta().GetUID())
133 err := updateFunc(func(obj interface{}) error {
134 state := obj.(*State)
135 state.NodeUID = nodeUID
136
137 return nil
138 })
139 if err != nil {
140 logging.NewLogger().Error(err, "failed to update cluster name in informer hook")
141 }
142 }
143
144
145
146
147 type State struct {
148
149
150
151
152 Hostname string `json:"hostname" binding:"required,hostname_rfc1123"`
153
154
155
156
157
158 NodeUID string `json:"node-uid"`
159
160
161 Network Network `json:"network"`
162
163
164 VNC VNCStates `json:"vnc"`
165
166 Kpower KpowerState `json:"kpower"`
167 }
168
169
170
171
172
173 type Network struct {
174
175
176
177 LANOutageDetected bool `json:"lan-outage-detected"`
178
179
180
181 LANOutageMode bool `json:"lan-outage-mode"`
182 }
183
184
185
186
187 func newState(ctx context.Context, cfg *config.Config) (*State, error) {
188 hostName := os.Getenv("NODE_NAME")
189
190 nodeUID, err := discoverNodeUID(ctx, cfg, hostName)
191 if err != nil {
192 return nil, fmt.Errorf("failed to discover node UID: %w", err)
193 }
194
195 lanOutageMode, err := discoverLANOutageMode(cfg.Fs)
196 if err != nil {
197 return nil, fmt.Errorf("failed to discover LAN outage mode: %w", err)
198 }
199
200 return &State{
201 Hostname: hostName,
202 NodeUID: nodeUID,
203 Network: Network{
204 LANOutageDetected: false,
205 LANOutageMode: lanOutageMode,
206 },
207 VNC: VNCStates{},
208 Kpower: KpowerState{
209 Status: UNKNOWN,
210 },
211 }, nil
212 }
213
214
215 func discoverNodeUID(ctx context.Context, cfg *config.Config, hostName string) (string, error) {
216 k8sClient := cfg.KubeRetryClient.Client()
217 hostNode := &v1.Node{}
218
219 err := k8sClient.Get(ctx, client.ObjectKey{Name: hostName}, hostNode)
220 if err != nil {
221 return "", err
222 }
223
224 uid := hostNode.GetObjectMeta().GetUID()
225 return string(uid), nil
226 }
227
228
229
230 func discoverLANOutageMode(fs afero.Fs) (bool, error) {
231 path, err := url.JoinPath(constants.ZynstraConfigDir, constants.LANOutageModeFlagFileName)
232 if err != nil {
233 return false, fmt.Errorf("failed to assemble LAN outage mode flag file path: %w", err)
234 }
235
236 isLOM, err := afero.Exists(fs, path)
237 if err != nil {
238 return false, fmt.Errorf("failed to read LAN outage mode state from flag file: %w", err)
239 }
240 return isLOM, nil
241 }
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265 type PatchParametersWrapper struct {
266
267
268
269 State State `json:"state"`
270 }
271
272
273
274
275 type StateResponseWrapper struct {
276
277
278
279 State State `json:"state"`
280 }
281
View as plain text