github.com/openshift/dpu-operator@v0.0.0-20240502153209-3af840d137c2/dpu-cni/pkgs/cniserver/cniserver.go (about)

     1  package cniserver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  	"time"
    15  
    16  	cni100 "github.com/containernetworking/cni/pkg/types/100"
    17  	"github.com/gorilla/mux"
    18  	"github.com/openshift/dpu-operator/dpu-cni/pkgs/cnihelper"
    19  	"github.com/openshift/dpu-operator/dpu-cni/pkgs/cnitypes"
    20  	"github.com/openshift/dpu-operator/dpu-cni/pkgs/sriov"
    21  	"k8s.io/klog/v2"
    22  )
    23  
    24  // This server implementations is only temporary for testing when the DPU Daemon
    25  // code has not been implemented.
    26  
    27  type processRequestFunc func(request *cnitypes.PodRequest) (*cni100.Result, error)
    28  type Server struct {
    29  	http.Server
    30  	cniCmdAddHandler processRequestFunc
    31  	cniCmdDelHandler processRequestFunc
    32  	runDir           string
    33  	socketPath       string
    34  }
    35  
    36  // ensureRunDirExists makes sure that the socket being created is only accessible to root.
    37  func ensureRunDirExists(serverSocketPath string) error {
    38  	runDir := filepath.Dir(serverSocketPath)
    39  	// Remove and re-create the socket directory with root-only permissions
    40  	klog.Infof("Removing %v", runDir)
    41  	if err := os.RemoveAll(runDir); err != nil && !os.IsNotExist(err) {
    42  		info, err := os.Stat(runDir)
    43  		if err != nil {
    44  			return fmt.Errorf("failed to stat old pod info socket directory %s: %v", runDir, err)
    45  		}
    46  		// Owner must be root
    47  		tmp := info.Sys()
    48  		statt, ok := tmp.(*syscall.Stat_t)
    49  		if !ok {
    50  			return fmt.Errorf("failed to read pod info socket directory stat info: %T", tmp)
    51  		}
    52  		if statt.Uid != 0 {
    53  			return fmt.Errorf("insecure owner of pod info socket directory %s: %v", runDir, statt.Uid)
    54  		}
    55  
    56  		// Check permissions
    57  		if info.Mode()&0o777 != 0o700 {
    58  			return fmt.Errorf("insecure permissions on pod info socket directory %s: %v", runDir, info.Mode())
    59  		}
    60  		// Finally remove the socket file so we can re-create it
    61  		if err := os.Remove(serverSocketPath); err != nil && !os.IsNotExist(err) {
    62  			return fmt.Errorf("failed to remove old pod info socket %s: %v", serverSocketPath, err)
    63  		}
    64  	}
    65  	klog.Infof("Creating %v", runDir)
    66  	if err := os.MkdirAll(runDir, 0o700); err != nil {
    67  		return fmt.Errorf("failed to create pod info socket directory %s: %v", runDir, err)
    68  	}
    69  	return nil
    70  }
    71  
    72  // Listen creates a listener to a unix socket located in `socketPath`
    73  func (s *Server) Listen() (net.Listener, error) {
    74  	err := ensureRunDirExists(s.socketPath)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("failed to create run directory for DPU CNI socket: %v", err)
    77  	}
    78  	listener, err := net.Listen("unix", filepath.Join(s.runDir, s.socketPath))
    79  	if err != nil {
    80  		return nil, fmt.Errorf("failed to listen on DPU CNI socket: %v", err)
    81  	}
    82  	klog.Info("Listen on socket path: ", s.socketPath)
    83  	if err := os.Chmod(s.socketPath, 0o600); err != nil {
    84  		_ = listener.Close()
    85  		return nil, fmt.Errorf("failed to set file permissions on DPU CNI socket: %v", err)
    86  	}
    87  	return listener, nil
    88  }
    89  
    90  func processRequest(request *cnitypes.Request) (*cni100.Result, error) {
    91  	// FIXME: Do actual work here.
    92  	klog.Infof("DEBUG: %v", request)
    93  
    94  	req, err := cniRequestToPodRequest(request)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	defer req.Cancel()
    99  	defer cniRequestEnvCleanup()
   100  
   101  	var res *cni100.Result = nil
   102  	sm := sriov.NewSriovManager()
   103  	if req.Command == cnitypes.CNIAdd {
   104  		res, err = sm.CmdAdd(req)
   105  	} else if req.Command == cnitypes.CNIDel {
   106  		err = sm.CmdDel(req)
   107  	}
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	return res, nil
   113  }
   114  
   115  // Split the "CNI_ARGS" environment variable's value into a map.  CNI_ARGS
   116  // contains arbitrary key/value pairs separated by ';' and is for runtime or
   117  // plugin specific uses.  Kubernetes passes the pod namespace and name in
   118  // CNI_ARGS.
   119  func gatherCNIArgs(env map[string]string) (map[string]string, error) {
   120  	cniArgs, ok := env["CNI_ARGS"]
   121  	if !ok {
   122  		return nil, fmt.Errorf("missing CNI_ARGS: '%s'", env)
   123  	}
   124  
   125  	mapArgs := make(map[string]string)
   126  	for _, arg := range strings.Split(cniArgs, ";") {
   127  		parts := strings.Split(arg, "=")
   128  		if len(parts) != 2 {
   129  			return nil, fmt.Errorf("invalid CNI_ARG '%s'", arg)
   130  		}
   131  		mapArgs[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
   132  	}
   133  	return mapArgs, nil
   134  }
   135  
   136  // cniRequestSetEnv sets the CNI environment variables. This is needed when delegating IPAM plugins.
   137  // Please see vendor/github.com/containernetworking/cni/pkg/invoke/delegate.go:delegateCommon()
   138  func cniRequestSetEnv(req *cnitypes.PodRequest) {
   139  	os.Setenv("CNI_COMMAND", req.Command)
   140  	os.Setenv("CNI_CONTAINERID", req.ContainerId)
   141  	os.Setenv("CNI_NETNS", req.Netns)
   142  	os.Setenv("CNI_IFNAME", req.IfName)
   143  	os.Setenv("CNI_PATH", req.Path)
   144  }
   145  
   146  // cniRequestEnvCleanup cleans up the CNI environment variables once delegating IPAM plugins is done.
   147  func cniRequestEnvCleanup() {
   148  	os.Unsetenv("CNI_COMMAND")
   149  	os.Unsetenv("CNI_CONTAINERID")
   150  	os.Unsetenv("CNI_NETNS")
   151  	os.Unsetenv("CNI_IFNAME")
   152  	os.Unsetenv("CNI_PATH")
   153  }
   154  
   155  // cniRequestToPodRequest
   156  func cniRequestToPodRequest(cr *cnitypes.Request) (*cnitypes.PodRequest, error) {
   157  	cmd, ok := cr.Env["CNI_COMMAND"]
   158  	if !ok {
   159  		return nil, fmt.Errorf("missing CNI_COMMAND")
   160  	}
   161  
   162  	req := &cnitypes.PodRequest{
   163  		Command: cmd,
   164  	}
   165  
   166  	req.ContainerId, ok = cr.Env["CNI_CONTAINERID"]
   167  	if !ok {
   168  		return nil, fmt.Errorf("missing CNI_CONTAINERID")
   169  	}
   170  
   171  	req.Netns, ok = cr.Env["CNI_NETNS"]
   172  	if !ok {
   173  		return nil, fmt.Errorf("missing CNI_NETNS")
   174  	}
   175  
   176  	req.IfName, ok = cr.Env["CNI_IFNAME"]
   177  	if !ok {
   178  		req.IfName = "eth0"
   179  	}
   180  
   181  	req.Path, ok = cr.Env["CNI_PATH"]
   182  	if !ok {
   183  		return nil, fmt.Errorf("missing CNI_PATH")
   184  	}
   185  
   186  	cniArgs, err := gatherCNIArgs(cr.Env)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	cniRequestSetEnv(req)
   192  
   193  	req.PodNamespace, ok = cniArgs["K8S_POD_NAMESPACE"]
   194  	if !ok {
   195  		return nil, fmt.Errorf("missing K8S_POD_NAMESPACE")
   196  	}
   197  	req.PodName, ok = cniArgs["K8S_POD_NAME"]
   198  	if !ok {
   199  		return nil, fmt.Errorf("missing K8S_POD_NAME")
   200  	}
   201  
   202  	// UID may not be passed by all runtimes yet. Will be passed
   203  	// by CRIO 1.20+ and containerd 1.5+ soon.
   204  	// CRIO 1.20: https://github.com/cri-o/cri-o/pull/5029
   205  	// CRIO 1.21: https://github.com/cri-o/cri-o/pull/5028
   206  	// CRIO 1.22: https://github.com/cri-o/cri-o/pull/5026
   207  	// containerd 1.6: https://github.com/containerd/containerd/pull/5640
   208  	// containerd 1.5: https://github.com/containerd/containerd/pull/5643
   209  	req.PodUID = cniArgs["K8S_POD_UID"]
   210  
   211  	conf, err := cnihelper.ReadCNIConfig(cr.Config)
   212  	if err != nil {
   213  		return nil, fmt.Errorf("broken stdin args")
   214  	}
   215  
   216  	req.NetName = conf.Name
   217  
   218  	if conf.DeviceID != "" {
   219  		// FIXME: Some DeviceIDs are formated differently between CNIs
   220  		// for instance the sriov CNI uses PCI address from the sriov device plugin
   221  		// and the nf CNI uses interface names from our internal device plugin
   222  		/*
   223  			if sriovtypes.IsPCIDeviceName(conf.DeviceID) {
   224  				// DeviceID is a PCI address
   225  			} else if sriovtypes.IsAuxDeviceName(conf.DeviceID) {
   226  				// DeviceID is an Auxiliary device name - <driver_name>.<kind_of_a_type>.<id>
   227  				chunks := strings.Split(conf.DeviceID, ".")
   228  				if chunks[1] != "sf" {
   229  					return nil, fmt.Errorf("only SF auxiliary devices are supported")
   230  				}
   231  			} else {
   232  				return nil, fmt.Errorf("expected PCI or Auxiliary device name, got - %s", conf.DeviceID)
   233  			}
   234  		*/
   235  	}
   236  
   237  	req.CNIConf = conf
   238  	req.DeviceInfo = cr.DeviceInfo
   239  	req.CNIReq = cr
   240  	req.Timestamp = time.Now()
   241  	// Match the Kubelet default CRI operation timeout of 2m
   242  	req.Ctx, req.Cancel = context.WithTimeout(context.Background(), 2*time.Minute)
   243  
   244  	fmt.Printf("%+v\n", req)
   245  	return req, nil
   246  }
   247  
   248  // handleCNIRequest will take the CNI request and delegate (TODO) work.
   249  func (s *Server) handleCNIRequest(r *http.Request) ([]byte, error) {
   250  	var cniRq cnitypes.Request
   251  	b, err := io.ReadAll(r.Body)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	if err := json.Unmarshal(b, &cniRq); err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	req, err := cniRequestToPodRequest(&cniRq)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	defer req.Cancel()
   264  
   265  	var result *cni100.Result = nil
   266  	if req.Command == cnitypes.CNIAdd {
   267  		result, err = s.cniCmdAddHandler(req)
   268  	} else if req.Command == cnitypes.CNIDel {
   269  		result, err = s.cniCmdDelHandler(req)
   270  	}
   271  	if err != nil {
   272  		klog.Errorf("Error occured in handler: %v", err)
   273  		return nil, err
   274  	}
   275  
   276  	response := &cnitypes.Response{Result: result}
   277  	return json.Marshal(&response)
   278  }
   279  
   280  // HttpCNIPost is a callback functions to handle "/cni" requests.
   281  func (s *Server) HttpCNIPost(w http.ResponseWriter, r *http.Request) {
   282  	if r.Method != http.MethodPost {
   283  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
   284  		return
   285  	}
   286  
   287  	result, err := s.handleCNIRequest(r)
   288  	if err != nil {
   289  		http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
   290  		return
   291  	}
   292  
   293  	w.WriteHeader(http.StatusOK)
   294  
   295  	// Empty response JSON means success with no body
   296  	w.Header().Set("Content-Type", "application/json")
   297  	if _, err := w.Write(result); err != nil {
   298  		klog.Errorf("Error writing HTTP response: %v", err)
   299  	}
   300  }
   301  
   302  // Start starts the server and begins serving on the given listener
   303  func (s *Server) ListenAndServe() error {
   304  	klog.Infof("Starting DPU CNI Server")
   305  	listener, err := s.Listen()
   306  	if err != nil {
   307  		klog.Errorf("Failed to start the CNI server using socket %s. Reason: %+v", cnitypes.ServerSocketPath, err)
   308  	}
   309  
   310  	klog.Infof("DPU CNI Server is now serving requests.")
   311  	if err := s.Serve(listener); err != nil {
   312  		klog.Errorf("DPU CNI server Serve() failed: %v", err)
   313  		return err
   314  	}
   315  	return nil
   316  }
   317  
   318  // NewCNIServer creates a new HTTP router instances to handle the CNI server requests.
   319  func NewCNIServer(addHandler processRequestFunc, delHandler processRequestFunc, options ...func(*Server)) *Server {
   320  	klog.Infof("DPU CNI Server creating new router.")
   321  	router := mux.NewRouter()
   322  	s := &Server{
   323  		Server: http.Server{
   324  			Handler: router,
   325  		},
   326  		cniCmdAddHandler: addHandler,
   327  		cniCmdDelHandler: delHandler,
   328  		socketPath:       cnitypes.ServerSocketPath,
   329  	}
   330  
   331  	router.NotFoundHandler = http.HandlerFunc(http.NotFound)
   332  	router.HandleFunc("/cni", http.HandlerFunc(s.HttpCNIPost))
   333  
   334  	for _, o := range options {
   335  		o(s)
   336  	}
   337  
   338  	return s
   339  }
   340  
   341  func WithSocketPath(socketPath string) func(*Server) {
   342  	return func(s *Server) {
   343  		s.socketPath = socketPath
   344  	}
   345  }