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"` +}