From fc5ed5ccea20edc4830e26f1e8cbbbce4f26fb37 Mon Sep 17 00:00:00 2001 From: Fabian Wiesel Date: Fri, 19 Dec 2025 12:15:54 +0100 Subject: [PATCH] Eviction: Use hypervisor cro instead of searching The hypervisor cro has the hypervisor-id, so let's use that one instead of looking it up via the name. --- internal/controller/eviction_controller.go | 66 ++--- .../controller/eviction_controller_test.go | 232 +++++++++--------- .../controller/onboarding_controller_test.go | 46 ++++ 3 files changed, 178 insertions(+), 166 deletions(-) diff --git a/internal/controller/eviction_controller.go b/internal/controller/eviction_controller.go index b8287ba..28d669d 100644 --- a/internal/controller/eviction_controller.go +++ b/internal/controller/eviction_controller.go @@ -29,7 +29,6 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/hypervisors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/services" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -40,7 +39,6 @@ import ( logger "sigs.k8s.io/controller-runtime/pkg/log" kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" - "github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/global" "github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack" ) @@ -61,7 +59,7 @@ const ( // +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions/status,verbs=get;update;patch // +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions/finalizers,verbs=update -// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch +// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -72,12 +70,11 @@ func (r *EvictionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, client.IgnoreNotFound(err) } - if global.LabelSelector != "" { - // This test-fetch the Node assigned to the eviction, it won't be cached if it's not part of our partition so - // we won't reconcile evictions for nodes outside our partition - if err := r.Get(ctx, types.NamespacedName{Name: eviction.Spec.Hypervisor}, &corev1.Node{}); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } + hv := &kvmv1.Hypervisor{} + // Let's fetch the Hypervisor assigned to the eviction, it won't be cached if it's not part of our partition so + // we won't reconcile evictions for nodes outside our partition + if err := r.Get(ctx, types.NamespacedName{Name: eviction.Spec.Hypervisor}, hv); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) } log := logger.FromContext(ctx). @@ -87,7 +84,7 @@ func (r *EvictionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // Being deleted if !eviction.DeletionTimestamp.IsZero() { - err := r.handleFinalizer(ctx, eviction) + err := r.handleFinalizer(ctx, eviction, hv) if err != nil { if errors.Is(err, ErrRetry) { return ctrl.Result{RequeueAfter: defaultWaitTime}, nil @@ -114,7 +111,7 @@ func (r *EvictionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c switch statusCondition.Status { case metav1.ConditionTrue: // We are running, so we need to evict the next instance - return r.handleRunning(ctx, eviction) + return r.handleRunning(ctx, eviction, hv) case metav1.ConditionFalse: // We are done, so we can just return log.Info("finished") @@ -129,10 +126,10 @@ func (r *EvictionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, nil } -func (r *EvictionReconciler) handleRunning(ctx context.Context, eviction *kvmv1.Eviction) (ctrl.Result, error) { +func (r *EvictionReconciler) handleRunning(ctx context.Context, eviction *kvmv1.Eviction, hypervisor *kvmv1.Hypervisor) (ctrl.Result, error) { if !meta.IsStatusConditionTrue(eviction.Status.Conditions, kvmv1.ConditionTypePreflight) { // Ensure the hypervisor is disabled and we have the preflight condition - return r.handlePreflight(ctx, eviction) + return r.handlePreflight(ctx, eviction, hypervisor) } // That should leave us with "Running" and the hypervisor should be deactivated @@ -158,30 +155,22 @@ func (r *EvictionReconciler) updateStatus(ctx context.Context, eviction, base *k client.MergeFromWithOptimisticLock{}), client.FieldOwner(EvictionControllerName)) } -func (r *EvictionReconciler) handlePreflight(ctx context.Context, eviction *kvmv1.Eviction) (ctrl.Result, error) { +func (r *EvictionReconciler) handlePreflight(ctx context.Context, eviction *kvmv1.Eviction, hv *kvmv1.Hypervisor) (ctrl.Result, error) { base := eviction.DeepCopy() - hypervisorName := eviction.Spec.Hypervisor - + expectHypervisor := HasStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeOnboarding) // Does the hypervisor even exist? Is it enabled/disabled? - hypervisor, err := openstack.GetHypervisorByName(ctx, r.computeClient, hypervisorName, false) + hypervisor, err := hypervisors.Get(ctx, r.computeClient, hv.Status.HypervisorID).Extract() if err != nil { - expectHypervisor := true - hv := &kvmv1.Hypervisor{} - if err := r.Get(ctx, client.ObjectKey{Name: eviction.Spec.Hypervisor}, hv); client.IgnoreNotFound(err) != nil { + if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) { return ctrl.Result{}, err } - if hv.Name != "" { - expectHypervisor = HasStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeOnboarding) - } - if expectHypervisor { // Abort eviction - err = fmt.Errorf("failed to get hypervisor %w", err) meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{ Type: kvmv1.ConditionTypeEvicting, Status: metav1.ConditionFalse, - Message: err.Error(), + Message: fmt.Sprintf("failed to get hypervisor %v", err), Reason: kvmv1.ConditionReasonFailed, }) return ctrl.Result{}, r.updateStatus(ctx, eviction, base) @@ -201,27 +190,14 @@ func (r *EvictionReconciler) handlePreflight(ctx context.Context, eviction *kvmv } } - log := logger.FromContext(ctx) - currentHypervisor, _, _ := strings.Cut(hypervisor.HypervisorHostname, ".") - if currentHypervisor != hypervisorName { - err = fmt.Errorf("hypervisor name %q does not match spec %q", currentHypervisor, hypervisorName) - log.Error(err, "Hypervisor name mismatch") - meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{ - Type: kvmv1.ConditionTypeEvicting, - Status: metav1.ConditionFalse, - Message: err.Error(), - Reason: kvmv1.ConditionReasonFailed, - }) - return ctrl.Result{}, r.updateStatus(ctx, eviction, base) - } - if !meta.IsStatusConditionTrue(eviction.Status.Conditions, kvmv1.ConditionTypeHypervisorDisabled) { // Hypervisor is not disabled/ensured, so we need to disable it return ctrl.Result{}, r.disableHypervisor(ctx, hypervisor, eviction) } // Fetch all virtual machines on the hypervisor - hypervisor, err = openstack.GetHypervisorByName(ctx, r.computeClient, hypervisorName, true) + trueVal := true + hypervisor, err = hypervisors.GetExt(ctx, r.computeClient, hv.Status.HypervisorID, hypervisors.GetOpts{WithServers: &trueVal}).Extract() if err != nil { return ctrl.Result{}, err } @@ -386,7 +362,7 @@ func (r *EvictionReconciler) evictionReason(eviction *kvmv1.Eviction) string { return fmt.Sprintf("Eviction %v/%v: %v", eviction.Namespace, eviction.Name, eviction.Spec.Reason) } -func (r *EvictionReconciler) handleFinalizer(ctx context.Context, eviction *kvmv1.Eviction) error { +func (r *EvictionReconciler) handleFinalizer(ctx context.Context, eviction *kvmv1.Eviction, hypervisor *kvmv1.Hypervisor) error { if !controllerutil.ContainsFinalizer(eviction, evictionFinalizerName) { return nil } @@ -395,7 +371,7 @@ func (r *EvictionReconciler) handleFinalizer(ctx context.Context, eviction *kvmv // - the hypervisor being gone, because it has been torn down // - the hypervisor having been enabled by someone else if !meta.IsStatusConditionTrue(eviction.Status.Conditions, kvmv1.ConditionTypeHypervisorReEnabled) { - err := r.enableHypervisorService(ctx, eviction) + err := r.enableHypervisorService(ctx, eviction, hypervisor) if err != nil { return err } @@ -406,10 +382,10 @@ func (r *EvictionReconciler) handleFinalizer(ctx context.Context, eviction *kvmv return r.Patch(ctx, eviction, client.MergeFromWithOptions(base, client.MergeFromWithOptimisticLock{})) } -func (r *EvictionReconciler) enableHypervisorService(ctx context.Context, eviction *kvmv1.Eviction) error { +func (r *EvictionReconciler) enableHypervisorService(ctx context.Context, eviction *kvmv1.Eviction, hv *kvmv1.Hypervisor) error { log := logger.FromContext(ctx) - hypervisor, err := openstack.GetHypervisorByName(ctx, r.computeClient, eviction.Spec.Hypervisor, false) + hypervisor, err := hypervisors.Get(ctx, r.computeClient, hv.Status.HypervisorID).Extract() if err != nil { if errors.Is(err, openstack.ErrNoHypervisor) { base := eviction.DeepCopy() diff --git a/internal/controller/eviction_controller_test.go b/internal/controller/eviction_controller_test.go index 0affc5e..3b9e45f 100644 --- a/internal/controller/eviction_controller_test.go +++ b/internal/controller/eviction_controller_test.go @@ -30,77 +30,50 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - ctrlRuntimeClient "sigs.k8s.io/controller-runtime/pkg/client" kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" ) -var HypervisorWithServers = `{ - "hypervisors": [ - { - "service": { - "host": "e6a37ee802d74863ab8b91ade8f12a67", - "id": "%s", - "disabled_reason": "%s" - }, - "cpu_info": { - "arch": "x86_64", - "model": "Nehalem", - "vendor": "Intel", - "features": [ - "pge", - "clflush" - ], - "topology": { - "cores": 1, - "threads": 1, - "sockets": 4 - } - }, - "current_workload": 0, - "status": "enabled", - "state": "up", - "disk_available_least": 0, - "host_ip": "1.1.1.1", - "free_disk_gb": 1028, - "free_ram_mb": 7680, - "hypervisor_hostname": %q, - "hypervisor_type": "fake", - "hypervisor_version": 2002000, - "id": "c48f6247-abe4-4a24-824e-ea39e108874f", - "local_gb": 1028, - "local_gb_used": 0, - "memory_mb": 8192, - "memory_mb_used": 512, - "running_vms": 0, - "vcpus": 1, - "vcpus_used": 0 - } - ] -}` - var _ = Describe("Eviction Controller", func() { const ( resourceName = "test-resource" namespaceName = "default" hypervisorName = "test-hypervisor" serviceId = "test-id" + hypervisorId = "test-hv-id" + hypervisorTpl = `{ + "hypervisor": { + "host_ip": "192.168.1.135", + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "test-hv-id", + "servers": [], + "service": { + "disabled_reason": %v, + "host": "compute", + "id": "test-id" + }, + "state": "up", + "status": "%v", + "uptime": null + } +}` ) var ( typeNamespacedName = types.NamespacedName{ Name: resourceName, Namespace: namespaceName, } + evictionObjectMeta = metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespaceName, + } reconcileRequest = ctrl.Request{NamespacedName: typeNamespacedName} controllerReconciler *EvictionReconciler fakeServer testhelper.FakeServer ) - BeforeEach(func() { - By("Setting up the OpenStack http mock server") - fakeServer = testhelper.SetupHTTP() - }) - AfterEach(func(ctx SpecContext) { resource := &kvmv1.Eviction{} err := k8sClient.Get(ctx, typeNamespacedName, resource) @@ -116,26 +89,13 @@ var _ = Describe("Eviction Controller", func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).Should(HaveOccurred()) } - - By("Tearing down the OpenStack http mock server") - fakeServer.Teardown() - controllerReconciler = nil - hv := &kvmv1.Hypervisor{ - ObjectMeta: metav1.ObjectMeta{ - Name: hypervisorName, - }, - } - Expect(ctrlRuntimeClient.IgnoreNotFound(k8sClient.Delete(ctx, hv))).To(Succeed()) }) Describe("API validation", func() { When("creating an eviction without hypervisor", func() { It("it should fail creating the resource", func(ctx SpecContext) { resource := &kvmv1.Eviction{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, + ObjectMeta: evictionObjectMeta, Spec: kvmv1.EvictionSpec{ Reason: "test-reason", }, @@ -148,10 +108,7 @@ var _ = Describe("Eviction Controller", func() { When("creating an eviction without reason", func() { It("it should fail creating the resource", func(ctx SpecContext) { resource := &kvmv1.Eviction{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, + ObjectMeta: evictionObjectMeta, Spec: kvmv1.EvictionSpec{ Hypervisor: hypervisorName, }, @@ -169,55 +126,89 @@ var _ = Describe("Eviction Controller", func() { Name: hypervisorName, }, } - Expect(ctrlRuntimeClient.IgnoreAlreadyExists(k8sClient.Create(ctx, hypervisor))).To(Succeed()) + Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, hypervisor)).To(Succeed()) + }) }) It("should successfully create the resource", func(ctx SpecContext) { - resource := &kvmv1.Eviction{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, + eviction := &kvmv1.Eviction{ + ObjectMeta: evictionObjectMeta, Spec: kvmv1.EvictionSpec{ Reason: "test-reason", Hypervisor: hypervisorName, }, } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Expect(k8sClient.Create(ctx, eviction)).To(Succeed()) + Expect(k8sClient.Delete(ctx, eviction)).To(Succeed()) }) }) }) Describe("Reconciliation", func() { - Describe("an eviction for 'test-hypervisor'", func() { + BeforeEach(func(ctx SpecContext) { + By("Setting up the OpenStack http mock server") + fakeServer = testhelper.SetupHTTP() + + DeferCleanup(func(ctx SpecContext) { + fakeServer.Teardown() + }) + + // Install default handler to fail unhandled requests + fakeServer.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + Fail("Unhandled request to fake server: " + r.Method + " " + r.URL.Path) + }) + + By("Creating the EvictionReconciler") + + controllerReconciler = &EvictionReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + computeClient: client.ServiceClient(fakeServer), + } + + DeferCleanup(func() { + controllerReconciler = nil + }) + }) + + Describe("an eviction for an onboarded 'test-hypervisor'", func() { BeforeEach(func(ctx SpecContext) { - By("Creating the resource") - resource := &kvmv1.Eviction{ + By("creating the hypervisor resource") + hypervisor := &kvmv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", + Name: hypervisorName, }, + } + Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, hypervisor)).To(Succeed()) + }) + + hypervisor.Status.HypervisorID = hypervisorId + meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{ + Type: kvmv1.ConditionTypeOnboarding, + Status: metav1.ConditionTrue, + Reason: "dontcare", + Message: "dontcare", + }) + Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed()) + + By("creating the eviction") + eviction := &kvmv1.Eviction{ + ObjectMeta: evictionObjectMeta, Spec: kvmv1.EvictionSpec{ Reason: "test-reason", Hypervisor: hypervisorName, }, } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - - By("Creating the controller") - controllerReconciler = &EvictionReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - computeClient: client.ServiceClient(fakeServer), - } + Expect(k8sClient.Create(ctx, eviction)).To(Succeed()) }) When("hypervisor is not found in openstack", func() { BeforeEach(func() { - fakeServer.Mux.HandleFunc("GET /os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - Expect(fmt.Fprintf(w, `{"hypervisors": []}`)).ToNot(BeNil()) + fakeServer.Mux.HandleFunc("GET /os-hypervisors/{hypervisor_id}", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) }) }) @@ -232,12 +223,12 @@ var _ = Describe("Eviction Controller", func() { Expect(err).NotTo(HaveOccurred()) // expect eviction condition to be false due to missing hypervisor - reconcileStatus := meta.FindStatusCondition(resource.Status.Conditions, kvmv1.ConditionTypeEvicting) - Expect(reconcileStatus).NotTo(BeNil()) - Expect(reconcileStatus.Status).To(Equal(metav1.ConditionFalse)) - Expect(reconcileStatus.Reason).To(Equal("Failed")) - Expect(reconcileStatus.Message).To(ContainSubstring("no hypervisor found")) - Expect(resource.Status.HypervisorServiceId).To(Equal("")) + Expect(resource.Status.Conditions).To(ContainElements(SatisfyAll( + HaveField("Status", metav1.ConditionFalse), + HaveField("Type", kvmv1.ConditionTypeEvicting), + HaveField("Reason", "Failed"), + HaveField("Message", ContainSubstring("got 404")), + ))) Expect(resource.GetFinalizers()).To(BeEmpty()) }) @@ -245,23 +236,23 @@ var _ = Describe("Eviction Controller", func() { }) When("enabled hypervisor has no servers", func() { BeforeEach(func(ctx SpecContext) { - fakeServer.Mux.HandleFunc("GET /os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { + fakeServer.Mux.HandleFunc("GET /os-hypervisors/{hypervisor_id}", func(w http.ResponseWriter, r *http.Request) { + rHypervisorId := r.PathValue("hypervisor_id") + Expect(rHypervisorId).To(Equal(hypervisorId)) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - Expect(fmt.Fprintf(w, HypervisorWithServers, serviceId, "", hypervisorName)).ToNot(BeNil()) + _, err := fmt.Fprintf(w, hypervisorTpl, "null", "enabled") + Expect(err).To(Succeed()) }) - fakeServer.Mux.HandleFunc("PUT /os-services/test-id", func(w http.ResponseWriter, r *http.Request) { + + fakeServer.Mux.HandleFunc("PUT /os-services/{service_id}", func(w http.ResponseWriter, r *http.Request) { + rServiceId := r.PathValue("service_id") + Expect(rServiceId).To(Equal(serviceId)) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - Expect(fmt.Fprintf(w, `{"service": {"id": "%v", "status": "disabled"}}`, serviceId)).ToNot(BeNil()) + _, err := fmt.Fprintf(w, `{"service": {"id": "%v", "status": "disabled"}}`, serviceId) + Expect(err).To(Succeed()) }) - By("creating the hypervisor resource") - hypervisor := &kvmv1.Hypervisor{ - ObjectMeta: metav1.ObjectMeta{ - Name: hypervisorName, - }, - } - Expect(ctrlRuntimeClient.IgnoreAlreadyExists(k8sClient.Create(ctx, hypervisor))).To(Succeed()) }) It("should succeed the reconciliation", func(ctx SpecContext) { runningCond := &metav1.Condition{ @@ -342,22 +333,21 @@ var _ = Describe("Eviction Controller", func() { }) When("disabled hypervisor has no servers", func() { BeforeEach(func(ctx SpecContext) { - fakeServer.Mux.HandleFunc("GET /os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { + fakeServer.Mux.HandleFunc("GET /os-hypervisors/{hypervisor_id}", func(w http.ResponseWriter, r *http.Request) { + rHypervisorId := r.PathValue("hypervisor_id") + Expect(rHypervisorId).To(Equal(hypervisorId)) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - Expect(fmt.Fprintf(w, HypervisorWithServers, serviceId, "some reason", hypervisorName)).ToNot(BeNil()) + _, err := fmt.Fprintf(w, hypervisorTpl, `"some reason"`, "disabled") + Expect(err).To(Succeed()) }) - fakeServer.Mux.HandleFunc("PUT /os-services/test-id", func(w http.ResponseWriter, r *http.Request) { + fakeServer.Mux.HandleFunc("PUT /os-services/{service_id}", func(w http.ResponseWriter, r *http.Request) { + rServiceId := r.PathValue("service_id") + Expect(rServiceId).To(Equal(serviceId)) w.WriteHeader(http.StatusOK) - Expect(fmt.Fprintf(w, `{"service": {"id": "%v", "status": "disabled"}}`, serviceId)).ToNot(BeNil()) + _, err := fmt.Fprintf(w, `{"service": {"id": "%v", "status": "disabled"}}`, serviceId) + Expect(err).To(Succeed()) }) - By("creating the hypervisor resource") - hypervisor := &kvmv1.Hypervisor{ - ObjectMeta: metav1.ObjectMeta{ - Name: hypervisorName, - }, - } - Expect(ctrlRuntimeClient.IgnoreAlreadyExists(k8sClient.Create(ctx, hypervisor))).To(Succeed()) }) It("should succeed the reconciliation", func(ctx SpecContext) { for range 3 { diff --git a/internal/controller/onboarding_controller_test.go b/internal/controller/onboarding_controller_test.go index 7cce58f..9c17894 100644 --- a/internal/controller/onboarding_controller_test.go +++ b/internal/controller/onboarding_controller_test.go @@ -35,6 +35,52 @@ import ( kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" ) +const ( + HypervisorWithServers = `{ + "hypervisors": [ + { + "service": { + "host": "e6a37ee802d74863ab8b91ade8f12a67", + "id": "%s", + "disabled_reason": "%s" + }, + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": %q, + "hypervisor_type": "fake", + "hypervisor_version": 2002000, + "id": "c48f6247-abe4-4a24-824e-ea39e108874f", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "vcpus": 1, + "vcpus_used": 0 + } + ] +}` +) + var _ = Describe("Onboarding Controller", func() { const ( region = "test-region"