From ed48bcb0fa60ffe8d7ed0f9e5a4f23c8ec17cf2b Mon Sep 17 00:00:00 2001 From: Igal Tsoiref Date: Thu, 26 Feb 2026 11:54:56 -0500 Subject: [PATCH] feat: add ConfigureHostVFs REST API endpoint to host agent Add a new POST /configure-host-vfs endpoint that triggers host network configuration by calling NetworkManager.AddNetworkRequest. This allows the DPU agent to request VF setup, bridge configuration, and PF network configuration via the host agent's REST API. Made-with: Cursor --- cmd/hostagent/subcmds/serve.go | 8 +- .../hostagent/service/installation_service.go | 53 ++++++++- .../service/installation_service_test.go | 108 +++++++++++++++++- .../hostagent/service/types/types.go | 5 + 4 files changed, 165 insertions(+), 9 deletions(-) diff --git a/cmd/hostagent/subcmds/serve.go b/cmd/hostagent/subcmds/serve.go index 9c525f11..3f1c86e6 100644 --- a/cmd/hostagent/subcmds/serve.go +++ b/cmd/hostagent/subcmds/serve.go @@ -71,10 +71,6 @@ var serveCmd = &cobra.Command{ klog.Fatalf("failed to convert VF config to network request: %v", err) } - if err := service.NewInstallationService(unCachedClient).Start(true); err != nil { - klog.Fatalf("failed to start installation service: %v", err) - } - mgr, err := ctrl.NewManager(clientCfg, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ @@ -107,6 +103,10 @@ var serveCmd = &cobra.Command{ os.Exit(1) } + if err := service.NewInstallationService(unCachedClient, nm).Start(true); err != nil { + klog.Fatalf("failed to start installation service: %v", err) + } + reconciler := hostagent.NewHostAgentReconciler(mgr.GetClient(), opts.BFBRegistryAddress, dpuNodeManager, nm) if err = reconciler.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DPU") diff --git a/internal/provisioning/hostagent/service/installation_service.go b/internal/provisioning/hostagent/service/installation_service.go index a85a56dd..487cd54a 100644 --- a/internal/provisioning/hostagent/service/installation_service.go +++ b/internal/provisioning/hostagent/service/installation_service.go @@ -63,19 +63,27 @@ const ( rpmRepoDir = "/rpm" ) +// NetworkConfigurator is an interface for triggering host network configuration. +// It is satisfied by networkmanager.NetworkManager. +type NetworkConfigurator interface { + AddNetworkRequest(dpu *provisioningv1.DPU) error +} + type InstallationService struct { client.Client handler http.Handler // mu protects listeners mu sync.Mutex // listeners maps interface names to their listeners - listeners map[string]net.Listener + listeners map[string]net.Listener + networkManager NetworkConfigurator } -func NewInstallationService(client client.Client) *InstallationService { +func NewInstallationService(client client.Client, nm NetworkConfigurator) *InstallationService { s := &InstallationService{ - Client: client, - listeners: make(map[string]net.Listener), + Client: client, + listeners: make(map[string]net.Listener), + networkManager: nm, } ws := new(restful.WebService).Path("/") ws.Route( @@ -92,6 +100,11 @@ func NewInstallationService(client client.Client) *InstallationService { Param(ws.QueryParameter("name", "the name of the object").Required(true)). Produces(restful.MIME_JSON). To(s.GetObject)) + ws.Route( + ws.POST("/configure-host-vfs"). + Consumes(restful.MIME_JSON). + Produces(restful.MIME_JSON). + To(s.ConfigureHostVFs)) ws.Route(ws.GET("/healthz").To(s.HealthCheck)) // Package repositories: serve .deb and .rpm packages for DPU provisioning. ws.Route(ws.GET("/deb/{subpath:*}").To(serveRepoFile(debRepoDir))) @@ -284,6 +297,38 @@ func (s *InstallationService) HealthCheck(req *restful.Request, resp *restful.Re resp.WriteHeader(http.StatusOK) } +func (s *InstallationService) ConfigureHostVFs(req *restful.Request, resp *restful.Response) { + var request types.ConfigureHostVFsRequest + if err := req.ReadEntity(&request); err != nil { + klog.Errorf("failed to read configure host VF request: %v", err) + _ = resp.WriteError(http.StatusBadRequest, err) + return + } + klog.Infof("Received configure host VF request: %#v", request) + + if s.networkManager == nil { + klog.Errorf("network manager is not configured") + _ = resp.WriteError(http.StatusServiceUnavailable, fmt.Errorf("network manager is not configured")) + return + } + + dpu := &provisioningv1.DPU{} + if err := s.Get(req.Request.Context(), client.ObjectKey{Namespace: request.DPUNamespace, Name: request.DPUName}, dpu); err != nil { + klog.Errorf("failed to get DPU %s/%s: %v", request.DPUNamespace, request.DPUName, err) + _ = resp.WriteError(http.StatusNotFound, err) + return + } + + if err := s.networkManager.AddNetworkRequest(dpu); err != nil { + klog.Errorf("failed to add network request for DPU %s/%s: %v", request.DPUNamespace, request.DPUName, err) + _ = resp.WriteError(http.StatusInternalServerError, err) + return + } + + klog.Infof("Successfully added network request for DPU %s/%s", request.DPUNamespace, request.DPUName) + resp.WriteHeader(http.StatusOK) +} + func (s *InstallationService) UpdateStatus(req *restful.Request, resp *restful.Response) { var request types.UpdateStatusRequest if err := req.ReadEntity(&request); err != nil { diff --git a/internal/provisioning/hostagent/service/installation_service_test.go b/internal/provisioning/hostagent/service/installation_service_test.go index 2bf4be95..2f02f3e2 100644 --- a/internal/provisioning/hostagent/service/installation_service_test.go +++ b/internal/provisioning/hostagent/service/installation_service_test.go @@ -34,6 +34,17 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +type mockNetworkConfigurator struct { + addNetworkRequestFunc func(dpu *provisioningv1.DPU) error +} + +func (m *mockNetworkConfigurator) AddNetworkRequest(dpu *provisioningv1.DPU) error { + if m.addNetworkRequestFunc != nil { + return m.addNetworkRequestFunc(dpu) + } + return nil +} + var _ = Describe("InstallationService", func() { var testNS *corev1.Namespace var installationService *InstallationService @@ -73,7 +84,7 @@ var _ = Describe("InstallationService", func() { testNS = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "installation-service-testns-"}} Expect(k8sClient.Create(ctx, testNS)).To(Succeed()) - installationService = NewInstallationService(k8sClient) + installationService = NewInstallationService(k8sClient, nil) Expect(installationService.Start(false)).To(Succeed()) // Start() runs the server in a goroutine; wait until it is listening to avoid connection refused. Eventually(func() error { @@ -291,4 +302,99 @@ var _ = Describe("InstallationService", func() { }) }) + Context("configure host VF", func() { + It("should return 400 when request body is malformed", func() { + resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBufferString("not-json")) + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) + }) + + It("should return 503 when network manager is not configured", func() { + dpu := createDPU("test-dpu", testNS.Name) + + request := types.ConfigureHostVFsRequest{ + DPUName: dpu.Name, + DPUNamespace: dpu.Namespace, + } + req, err := json.Marshal(request) + Expect(err).To(Succeed()) + + resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req)) + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusServiceUnavailable)) + }) + + Context("when network manager is configured", func() { + var mockNM *mockNetworkConfigurator + + BeforeEach(func() { + installationService.Stop() + mockNM = &mockNetworkConfigurator{} + installationService = NewInstallationService(k8sClient, mockNM) + Expect(installationService.Start(false)).To(Succeed()) + }) + + It("should successfully configure host VF", func() { + dpu := createDPU("test-dpu", testNS.Name) + + var receivedDPU *provisioningv1.DPU + mockNM.addNetworkRequestFunc = func(dpu *provisioningv1.DPU) error { + receivedDPU = dpu + return nil + } + + request := types.ConfigureHostVFsRequest{ + DPUName: dpu.Name, + DPUNamespace: dpu.Namespace, + } + req, err := json.Marshal(request) + Expect(err).To(Succeed()) + + resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req)) + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(receivedDPU).NotTo(BeNil()) + Expect(receivedDPU.Name).To(Equal(dpu.Name)) + Expect(receivedDPU.Namespace).To(Equal(dpu.Namespace)) + + By("the full DPU spec should be passed to AddNetworkRequest") + Expect(receivedDPU.Spec.SerialNumber).To(Equal(dpu.Spec.SerialNumber)) + Expect(receivedDPU.Spec.DPUFlavor).To(Equal(dpu.Spec.DPUFlavor)) + Expect(receivedDPU.Spec.BFB).To(Equal(dpu.Spec.BFB)) + }) + + It("should return 404 when DPU not found", func() { + request := types.ConfigureHostVFsRequest{ + DPUName: "non-existent-dpu", + DPUNamespace: testNS.Name, + } + req, err := json.Marshal(request) + Expect(err).To(Succeed()) + + resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req)) + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) + }) + + It("should return 500 when AddNetworkRequest fails", func() { + dpu := createDPU("test-dpu", testNS.Name) + + mockNM.addNetworkRequestFunc = func(dpu *provisioningv1.DPU) error { + return fmt.Errorf("network manager is not initialized") + } + + request := types.ConfigureHostVFsRequest{ + DPUName: dpu.Name, + DPUNamespace: dpu.Namespace, + } + req, err := json.Marshal(request) + Expect(err).To(Succeed()) + + resp, err := http.Post(fmt.Sprintf("http://%s/configure-host-vfs", address), "application/json", bytes.NewBuffer(req)) + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError)) + }) + }) + }) + }) diff --git a/internal/provisioning/hostagent/service/types/types.go b/internal/provisioning/hostagent/service/types/types.go index f38953e8..28ef63e2 100644 --- a/internal/provisioning/hostagent/service/types/types.go +++ b/internal/provisioning/hostagent/service/types/types.go @@ -25,3 +25,8 @@ type UpdateStatusRequest struct { DPUNamespace string `json:"dpuNamespace"` AgentStatus provisioningv1.AgentStatus `json:"agentStatus"` } + +type ConfigureHostVFsRequest struct { + DPUName string `json:"dpuName"` + DPUNamespace string `json:"dpuNamespace"` +}