...

Source file src/edge-infra.dev/pkg/sds/interlock/topic/host/host.go

Documentation: edge-infra.dev/pkg/sds/interlock/topic/host

     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  	// Host represents the local node.
    28  	TopicName = "host"
    29  
    30  	Path = "/v1/host"
    31  )
    32  
    33  // Host represents the local node
    34  type Host struct {
    35  	topic topic.Topic
    36  }
    37  
    38  // New returns a new Host and configures the topic with the name, initialized
    39  // state and websocket manager
    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  // RegistersEndpoints registers the Host topic endpoints on the provided router
    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  // SetupAPIInformers adds the NodeUIDEventHandler to the Node informer
    79  // present in the config that is used in the Interlock API server.
    80  // This allows the Interlock's host topic to respond to Node events appropriately
    81  func (h *Host) SetupAPIInformers(ctx context.Context, cfg *config.Config) error {
    82  	// Get nodes informer
    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  	// Create event handler for the host node UID
    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  // IsHostNode returns true if the provided object is the node this interlock instance is on
   101  // otherwise false
   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  // NodeUIDEventHandler handles keeping the host node UID up to date by watching
   111  // Kubernetes events
   112  type NodeUIDEventHandler struct {
   113  	UpdateFunc func(topic.UpdateFunc) error
   114  }
   115  
   116  // OnAdd updates the host node-uid on Node create events
   117  func (nueh *NodeUIDEventHandler) OnAdd(obj interface{}, _ bool) {
   118  	node := obj.(*v1.Node)
   119  	updateNodeUID(nueh.UpdateFunc, node)
   120  }
   121  
   122  // OnUpdate ignores update events since uid is a read-only field
   123  func (nueh *NodeUIDEventHandler) OnUpdate(_, _ interface{}) {
   124  }
   125  
   126  // OnDelete ignores delete events
   127  func (nueh *NodeUIDEventHandler) OnDelete(_ interface{}) {}
   128  
   129  // updateNodeUID retrieves the node UID from the given Node object and assigns
   130  // it to the host node-uid field
   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  // State contains information about the local node
   145  //
   146  // swagger:model HostState
   147  type State struct {
   148  	// Hostname of the host node. This field is read only and will be kept up to date internally by Interlock
   149  	//
   150  	// readOnly: true
   151  	// example: s1-master-1
   152  	Hostname string `json:"hostname" binding:"required,hostname_rfc1123"`
   153  
   154  	// UID of the host node. This field is read only and will be kept up to date internally by Interlock
   155  	//
   156  	// readOnly: true
   157  	// example: abcdefgh-3aee-438b-a2a8-b4de3a8f470f
   158  	NodeUID string `json:"node-uid"`
   159  
   160  	// Network status of the host node
   161  	Network Network `json:"network"`
   162  
   163  	// VNCStates contains the VNC states of the host node
   164  	VNC VNCStates `json:"vnc"`
   165  
   166  	Kpower KpowerState `json:"kpower"`
   167  }
   168  
   169  // Network contains information about the network status of the local
   170  // node
   171  //
   172  // swagger:model
   173  type Network struct {
   174  	// Whether or not a current LAN outage has been detected
   175  	//
   176  	// required: true
   177  	LANOutageDetected bool `json:"lan-outage-detected"`
   178  	// Whether or not the host node is currently in LAN outage mode
   179  	//
   180  	// required: true
   181  	LANOutageMode bool `json:"lan-outage-mode"`
   182  }
   183  
   184  // newState returns a new host State with initialized values
   185  // LANOutageMode state is discovered from the host filesystem
   186  // NodeUID is discovered from the kubernetes Node resource
   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  // discoverNodeUID gets the uid from the host Node object
   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  // discoverLANOutageMode reads the LAN outage mode state from the filesystem
   229  // of the host node
   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  // Everything below this point is used for documentation purposes and exists for
   244  // the purpose of generating the swagger spec only.
   245  
   246  // swagger:route GET /v1/host host GetHostState
   247  // Retrieve the current host state
   248  // responses:
   249  //   200: HostStateResponse
   250  //   404: ErrorResponse
   251  //   405: ErrorResponse
   252  //   500: ErrorResponse
   253  
   254  // swagger:route PATCH /v1/host host PatchHostState
   255  // Patch the host state
   256  // responses:
   257  //   202: HostStateResponse
   258  //   404: ErrorResponse
   259  //   405: ErrorResponse
   260  //   500: ErrorResponse
   261  
   262  // The PatchHostState parameters
   263  //
   264  // swagger:parameters PatchHostState
   265  type PatchParametersWrapper struct {
   266  	// The host state that the client wants to patch
   267  	//
   268  	// in:body
   269  	State State `json:"state"`
   270  }
   271  
   272  // The request was successful
   273  //
   274  // swagger:response HostStateResponse
   275  type StateResponseWrapper struct {
   276  	// The current host state
   277  	//
   278  	// in:body
   279  	State State `json:"state"`
   280  }
   281  

View as plain text