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 }