Skip to content

Commit 1504dc1

Browse files
committed
HypervisorMaintenanceController: Enable/Disable compute service
The controller only gets active after onboarding, as that one needs to take care of enabling and aggregate association depending on the tests. If maintenance is set, it will disable now the compute host in nova, and enable it, if it is unset. It will only do so on an "edge", i.e. if it hasn't done it before.
1 parent 387fb81 commit 1504dc1

File tree

2 files changed

+288
-0
lines changed

2 files changed

+288
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2024 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package controller
19+
20+
// This controller only takes care of enabling or disabling the compute
21+
// service depending on the hypervisor spec Maintenance field
22+
23+
import (
24+
"context"
25+
"fmt"
26+
27+
"k8s.io/apimachinery/pkg/api/meta"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
32+
logger "sigs.k8s.io/controller-runtime/pkg/log"
33+
34+
"github.com/gophercloud/gophercloud/v2"
35+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/services"
36+
37+
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
38+
"github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack"
39+
)
40+
41+
const (
42+
HypervisorMaintenanceControllerName = "HypervisorMaintenanceController"
43+
)
44+
45+
type HypervisorMaintenanceController struct {
46+
k8sclient.Client
47+
Scheme *runtime.Scheme
48+
computeClient *gophercloud.ServiceClient
49+
}
50+
51+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch
52+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors/status,verbs=get;list;watch;create;update;patch;delete
53+
54+
func (hec *HypervisorMaintenanceController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
55+
hv := &kvmv1.Hypervisor{}
56+
if err := hec.Get(ctx, req.NamespacedName, hv); err != nil {
57+
// OnboardingReconciler not found errors, could be deleted
58+
return ctrl.Result{}, k8sclient.IgnoreNotFound(err)
59+
}
60+
61+
// is onboarding completed?
62+
if !meta.IsStatusConditionFalse(hv.Status.Conditions, ConditionTypeOnboarding) {
63+
return ctrl.Result{}, nil
64+
}
65+
66+
// ensure serviceId is set
67+
if hv.Status.ServiceID == "" {
68+
return ctrl.Result{}, nil
69+
}
70+
71+
log := logger.FromContext(ctx).
72+
WithName("HypervisorService")
73+
ctx = logger.IntoContext(ctx, log)
74+
75+
changed, err := hec.handleSpecMaintenance(ctx, hv)
76+
if err != nil {
77+
return ctrl.Result{}, err
78+
}
79+
80+
if changed {
81+
return ctrl.Result{}, hec.Status().Update(ctx, hv)
82+
} else {
83+
return ctrl.Result{}, nil
84+
}
85+
}
86+
87+
func (hec *HypervisorMaintenanceController) handleSpecMaintenance(ctx context.Context, hv *kvmv1.Hypervisor) (bool, error) {
88+
log := logger.FromContext(ctx)
89+
serviceId := hv.Status.ServiceID
90+
91+
switch hv.Spec.Maintenance {
92+
case "": // Enable the compute service (in case we haven't done so already)
93+
if !meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
94+
Type: kvmv1.ConditionTypeHypervisorDisabled,
95+
Status: metav1.ConditionFalse,
96+
Message: "Hypervisor enabled",
97+
Reason: kvmv1.ConditionReasonSucceeded,
98+
}) {
99+
// Spec matches status
100+
return false, nil
101+
}
102+
// We need to enable the host as per spec
103+
enableService := services.UpdateOpts{Status: services.ServiceEnabled}
104+
log.Info("Enabling hypervisor", "id", serviceId)
105+
_, err := services.Update(ctx, hec.computeClient, serviceId, enableService).Extract()
106+
if err != nil {
107+
return false, fmt.Errorf("failed to enable hypervisor due to %w", err)
108+
}
109+
case "manual", "auto", "ha": // Disable the compute service
110+
if !meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
111+
Type: kvmv1.ConditionTypeHypervisorDisabled,
112+
Status: metav1.ConditionTrue,
113+
Message: "Hypervisor disabled",
114+
Reason: kvmv1.ConditionReasonSucceeded,
115+
}) {
116+
// Spec matches status
117+
return false, nil
118+
}
119+
120+
// We need to disable the host as per spec
121+
enableService := services.UpdateOpts{Status: services.ServiceDisabled, DisabledReason: ""}
122+
log.Info("Disabling hypervisor", "id", serviceId)
123+
_, err := services.Update(ctx, hec.computeClient, serviceId, enableService).Extract()
124+
if err != nil {
125+
return false, fmt.Errorf("failed to disable hypervisor due to %w", err)
126+
}
127+
}
128+
129+
return true, nil
130+
}
131+
132+
// SetupWithManager sets up the controller with the Manager.
133+
func (hec *HypervisorMaintenanceController) SetupWithManager(mgr ctrl.Manager) error {
134+
ctx := context.Background()
135+
_ = logger.FromContext(ctx)
136+
137+
var err error
138+
if hec.computeClient, err = openstack.GetServiceClient(ctx, "compute", nil); err != nil {
139+
return err
140+
}
141+
hec.computeClient.Microversion = "2.90" // Xena (or later)
142+
143+
return ctrl.NewControllerManagedBy(mgr).
144+
Named(HypervisorMaintenanceControllerName).
145+
For(&kvmv1.Hypervisor{}).
146+
Complete(hec)
147+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package controller
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"net/http"
24+
25+
"github.com/gophercloud/gophercloud/v2/testhelper"
26+
"github.com/gophercloud/gophercloud/v2/testhelper/client"
27+
. "github.com/onsi/ginkgo/v2"
28+
. "github.com/onsi/gomega"
29+
"k8s.io/apimachinery/pkg/api/meta"
30+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
32+
ctrl "sigs.k8s.io/controller-runtime"
33+
34+
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
35+
)
36+
37+
var _ = Describe("HypervisorServiceController", func() {
38+
var (
39+
tc *HypervisorMaintenanceController
40+
fakeServer testhelper.FakeServer
41+
hypervisorName = types.NamespacedName{Name: "hv-test"}
42+
)
43+
44+
const (
45+
ServiceEnabledResponse = `{
46+
"service": {
47+
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
48+
"binary": "nova-compute",
49+
"disabled_reason": "maintenance",
50+
"host": "host1",
51+
"state": "up",
52+
"status": "disabled",
53+
"updated_at": "2012-10-29T13:42:05.000000",
54+
"forced_down": false,
55+
"zone": "nova"
56+
}
57+
}`
58+
)
59+
60+
// Setup and teardown
61+
BeforeEach(func(ctx context.Context) {
62+
By("Setting up the OpenStack http mock server")
63+
fakeServer = testhelper.SetupHTTP()
64+
65+
By("Creating the HypervisorServiceController")
66+
tc = &HypervisorMaintenanceController{
67+
Client: k8sClient,
68+
Scheme: k8sClient.Scheme(),
69+
computeClient: client.ServiceClient(fakeServer),
70+
}
71+
72+
By("Creating a blank Hypervisor resource")
73+
hypervisor := &kvmv1.Hypervisor{
74+
ObjectMeta: v1.ObjectMeta{
75+
Name: hypervisorName.Name,
76+
Namespace: hypervisorName.Namespace,
77+
},
78+
Spec: kvmv1.HypervisorSpec{},
79+
}
80+
Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed())
81+
})
82+
83+
AfterEach(func() {
84+
By("Deleting the Hypervisor resource")
85+
hypervisor := &kvmv1.Hypervisor{}
86+
Expect(tc.Client.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
87+
Expect(tc.Client.Delete(ctx, hypervisor)).To(Succeed())
88+
89+
By("Tearing down the OpenStack http mock server")
90+
fakeServer.Teardown()
91+
})
92+
93+
// Tests
94+
95+
Context("Reconcile Spec.Maintenance=\"\"", func() {
96+
BeforeEach(func() {
97+
hypervisor := &kvmv1.Hypervisor{}
98+
Expect(tc.Client.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
99+
hypervisor.Status.ServiceID = "1234"
100+
meta.SetStatusCondition(&hypervisor.Status.Conditions,
101+
v1.Condition{
102+
Type: ConditionTypeOnboarding,
103+
Status: v1.ConditionFalse,
104+
Reason: v1.StatusSuccess,
105+
Message: "random text",
106+
},
107+
)
108+
109+
Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed())
110+
111+
// Mock services.Update
112+
fakeServer.Mux.HandleFunc("PUT /os-services/1234", func(w http.ResponseWriter, r *http.Request) {
113+
// parse request
114+
Expect(r.Method).To(Equal("PUT"))
115+
Expect(r.Header.Get("Content-Type")).To(Equal("application/json"))
116+
117+
// verify request body
118+
expectedBody := `{"status": "enabled"}`
119+
body := make([]byte, r.ContentLength)
120+
_, err := r.Body.Read(body)
121+
Expect(err == nil || err.Error() == "EOF").To(BeTrue())
122+
Expect(string(body)).To(MatchJSON(expectedBody))
123+
124+
w.WriteHeader(http.StatusOK)
125+
_, err = fmt.Fprint(w, ServiceEnabledResponse)
126+
Expect(err).NotTo(HaveOccurred())
127+
})
128+
})
129+
130+
It("should enable the service", func() {
131+
req := ctrl.Request{NamespacedName: hypervisorName}
132+
_, err := tc.Reconcile(ctx, req)
133+
Expect(err).NotTo(HaveOccurred())
134+
135+
updated := &kvmv1.Hypervisor{}
136+
Expect(tc.Client.Get(ctx, req.NamespacedName, updated)).To(Succeed())
137+
GinkgoWriter.Printf("Updated Conditions: %+v", updated.Status.Conditions)
138+
Expect(meta.IsStatusConditionFalse(updated.Status.Conditions, kvmv1.ConditionTypeHypervisorDisabled)).To(BeTrue())
139+
})
140+
})
141+
})

0 commit comments

Comments
 (0)