Skip to content

Commit 4adf46b

Browse files
DanNiEShclaude
andcommitted
Replace internal/ironic with backend-agnostic internal/management interface
Introduce a management.Client interface with GetPowerState/SetPowerState methods and a config-driven factory/registry pattern, mirroring the inventory interface in bare-metal-fulfillment-operator. The OpenStack implementation wraps gophercloud Ironic calls. The controller and cmd/main.go are updated to use management.Client, and internal/ironic/ is removed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e923abb commit 4adf46b

8 files changed

Lines changed: 428 additions & 493 deletions

File tree

cmd/main.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import (
4545

4646
v1alpha1 "github.com/osac-project/bare-metal-fulfillment-operator/api/v1alpha1"
4747
"github.com/osac-project/host-management-openstack/internal/controller"
48-
"github.com/osac-project/host-management-openstack/internal/ironic"
48+
"github.com/osac-project/host-management-openstack/internal/management"
4949
"github.com/osac-project/osac-operator/pkg/aap"
5050
"github.com/osac-project/osac-operator/pkg/provisioning"
5151
// +kubebuilder:scaffold:imports
@@ -233,15 +233,16 @@ func main() {
233233
os.Exit(1)
234234
}
235235

236-
// Ironic client for bare metal management
237-
var ironicClient *ironic.Client
238-
ironicCtx, ironicCancel := context.WithTimeout(context.Background(), 30*time.Second)
239-
defer ironicCancel()
240-
if ironicClient, err = ironic.NewClient(ironicCtx); err != nil {
241-
setupLog.Error(err, "failed to create Ironic client")
236+
mgmtCtx, mgmtCancel := context.WithTimeout(context.Background(), 30*time.Second)
237+
defer mgmtCancel()
238+
managementClient, err := management.NewOpenStackClient(mgmtCtx, &management.Config{
239+
Type: "openstack",
240+
})
241+
if err != nil {
242+
setupLog.Error(err, "failed to create management client")
242243
os.Exit(1)
243244
}
244-
setupLog.Info("Connect to ironic", "endpoint", ironicClient.GetEndpoint())
245+
setupLog.Info("Management client created", "type", "openstack")
245246

246247
// AAP provisioning provider for image provisioning workflows
247248
var provisioningProvider provisioning.ProvisioningProvider
@@ -272,7 +273,7 @@ func main() {
272273
hostLeaseReconciler := controller.NewHostLeaseReconciler(
273274
mgr.GetClient(),
274275
mgr.GetScheme(),
275-
ironicClient,
276+
managementClient,
276277
provisioningProvider,
277278
0, // Use DefaultRecheckInterval
278279
)

internal/controller/hostlease_controller.go

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"time"
2525

2626
"github.com/go-logr/logr"
27-
"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
2827
"k8s.io/apimachinery/pkg/api/equality"
2928
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3029
"k8s.io/apimachinery/pkg/runtime"
@@ -37,30 +36,24 @@ import (
3736
"sigs.k8s.io/controller-runtime/pkg/predicate"
3837

3938
v1alpha1 "github.com/osac-project/bare-metal-fulfillment-operator/api/v1alpha1"
40-
"github.com/osac-project/host-management-openstack/internal/ironic"
39+
"github.com/osac-project/host-management-openstack/internal/management"
4140
opv1alpha1 "github.com/osac-project/osac-operator/api/v1alpha1"
4241
"github.com/osac-project/osac-operator/pkg/provisioning"
4342
)
4443

45-
// HostLeaseReconciler reconciles HostLease CRs for power management via Ironic.
4644
type HostLeaseReconciler struct {
4745
client.Client
48-
Scheme *runtime.Scheme
49-
IronicClient ironic.NodeClient
50-
ProvisioningProvider provisioning.ProvisioningProvider
51-
52-
// RecheckInterval is the interval for polling Ironic until power state matches desired state.
53-
RecheckInterval time.Duration
54-
55-
// ProvisionPollInterval is the interval for polling AAP job status.
46+
Scheme *runtime.Scheme
47+
ManagementClient management.Client
48+
ProvisioningProvider provisioning.ProvisioningProvider
49+
RecheckInterval time.Duration
5650
ProvisionPollInterval time.Duration
5751
}
5852

59-
// NewHostLeaseReconciler creates a new HostLeaseReconciler with defaults applied.
6053
func NewHostLeaseReconciler(
6154
client client.Client,
6255
scheme *runtime.Scheme,
63-
ironicClient ironic.NodeClient,
56+
managementClient management.Client,
6457
provider provisioning.ProvisioningProvider,
6558
recheckInterval time.Duration,
6659
) *HostLeaseReconciler {
@@ -71,7 +64,7 @@ func NewHostLeaseReconciler(
7164
return &HostLeaseReconciler{
7265
Client: client,
7366
Scheme: scheme,
74-
IronicClient: ironicClient,
67+
ManagementClient: managementClient,
7568
ProvisioningProvider: provider,
7669
RecheckInterval: recheckInterval,
7770
ProvisionPollInterval: DefaultProvisionPollInterval,
@@ -143,34 +136,44 @@ func (r *HostLeaseReconciler) handleUpdate(ctx context.Context, hostLease *v1alp
143136
}
144137
}
145138

146-
node, err := r.IronicClient.GetNode(ctx, hostLease.Spec.ExternalHostID)
139+
powerStatus, err := r.ManagementClient.GetPowerState(ctx, hostLease.Spec.ExternalHostID)
147140
if err != nil {
148-
log.Error(err, "failed to get Ironic node", "nodeID", hostLease.Spec.ExternalHostID)
141+
log.Error(err, "failed to get power state", "nodeID", hostLease.Spec.ExternalHostID)
142+
r.syncHostLeaseStatus(hostLease, nil, err, log)
143+
return ctrl.Result{}, err
144+
}
145+
if powerStatus == nil {
146+
err := fmt.Errorf("management backend returned nil power status for host %s", hostLease.Spec.ExternalHostID)
147+
log.Error(err, "unexpected nil power status", "nodeID", hostLease.Spec.ExternalHostID)
149148
r.syncHostLeaseStatus(hostLease, nil, err, log)
150149
return ctrl.Result{}, err
151150
}
152-
log.V(1).Info("Ironic node", "nodeID", hostLease.Spec.ExternalHostID, "power_state", node.PowerState)
151+
log.V(1).Info("Host power state", "nodeID", hostLease.Spec.ExternalHostID, "power_state", powerStatus.State)
153152

154153
if hostLease.Spec.PoweredOn != nil {
155-
if err := r.reconcilePower(ctx, hostLease, node, log); err != nil {
154+
if err := r.reconcilePower(ctx, hostLease, powerStatus, log); err != nil {
156155
r.syncHostLeaseStatus(hostLease, nil, err, log)
157156
return ctrl.Result{}, err
158157
}
159158

160-
node, err = r.IronicClient.GetNode(ctx, hostLease.Spec.ExternalHostID)
159+
powerStatus, err = r.ManagementClient.GetPowerState(ctx, hostLease.Spec.ExternalHostID)
161160
if err != nil {
162-
log.Error(err, "failed to refresh node after power reconciliation", "nodeID", hostLease.Spec.ExternalHostID)
161+
log.Error(err, "failed to refresh power state after reconciliation", "nodeID", hostLease.Spec.ExternalHostID)
162+
r.syncHostLeaseStatus(hostLease, nil, err, log)
163+
return ctrl.Result{}, err
164+
}
165+
if powerStatus == nil {
166+
err := fmt.Errorf("management backend returned nil power status for host %s", hostLease.Spec.ExternalHostID)
167+
log.Error(err, "unexpected nil power status after reconciliation", "nodeID", hostLease.Spec.ExternalHostID)
163168
r.syncHostLeaseStatus(hostLease, nil, err, log)
164169
return ctrl.Result{}, err
165170
}
166171
}
167172

168-
r.syncHostLeaseStatus(hostLease, node, nil, log)
173+
r.syncHostLeaseStatus(hostLease, powerStatus, nil, log)
169174

170175
if hostLease.Spec.PoweredOn != nil {
171-
currentlyOn := node.PowerState == ironic.PowerOn.String()
172-
if *hostLease.Spec.PoweredOn != currentlyOn {
173-
hostLease.Status.Phase = v1alpha1.HostLeasePhaseProgressing
176+
if *hostLease.Spec.PoweredOn != (powerStatus.State == management.PowerOn) {
174177
return ctrl.Result{RequeueAfter: r.RecheckInterval}, nil
175178
}
176179
}
@@ -303,38 +306,41 @@ func (r *HostLeaseReconciler) validateOpenStackHost(hostLease *v1alpha1.HostLeas
303306
return true
304307
}
305308

306-
func (r *HostLeaseReconciler) reconcilePower(ctx context.Context, hostLease *v1alpha1.HostLease, node *nodes.Node, log logr.Logger) error {
307-
currentlyOn := node.PowerState == ironic.PowerOn.String()
309+
func (r *HostLeaseReconciler) reconcilePower(ctx context.Context, hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, log logr.Logger) error {
310+
currentlyOn := powerStatus.State == management.PowerOn
308311
desiredOn := *hostLease.Spec.PoweredOn
309312

310-
// If Ironic is already processing a power state change, skip to avoid 409 Conflict.
311-
if r.IronicClient.IsNodePowerTransitioning(node) {
313+
if powerStatus.IsTransitioning {
312314
log.V(1).Info("Node is transitioning, skipping power action",
313-
"nodeID", hostLease.Spec.ExternalHostID,
314-
"targetPowerState", node.TargetPowerState)
315+
"nodeID", hostLease.Spec.ExternalHostID)
315316
return nil
316317
}
317318

318-
var err error
319319
needsPowerUpdate := desiredOn != currentlyOn
320320
if !needsPowerUpdate {
321-
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", node.PowerState)
321+
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", powerStatus.State)
322322
return nil
323323
}
324324

325-
targetState := ironic.PowerOff
325+
targetState := management.PowerOff
326326
action := "off"
327327
if desiredOn {
328-
targetState = ironic.PowerOn
328+
targetState = management.PowerOn
329329
action = "on"
330330
}
331331

332332
log.Info("Powering "+action+" node", "nodeID", hostLease.Spec.ExternalHostID)
333-
if err = r.IronicClient.SetPowerState(ctx, hostLease.Spec.ExternalHostID, targetState); err != nil {
333+
if err := r.ManagementClient.SetPowerState(ctx, hostLease.Spec.ExternalHostID, targetState); err != nil {
334+
if errors.Is(err, management.ErrTransitioning) {
335+
log.Info("Node is transitioning (conflict), will retry",
336+
"nodeID", hostLease.Spec.ExternalHostID)
337+
return nil
338+
}
334339
log.Error(err, "failed to power "+action+" node", "nodeID", hostLease.Spec.ExternalHostID)
340+
return err
335341
}
336342

337-
return err
343+
return nil
338344
}
339345

340346
func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLease *v1alpha1.HostLease) (ctrl.Result, error) {
@@ -393,8 +399,7 @@ func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLea
393399
return result, nil
394400
}
395401

396-
// syncHostLeaseStatus syncs power-related conditions and observed power state in memory.
397-
func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease, node *nodes.Node, reconcileErr error, log logr.Logger) {
402+
func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, reconcileErr error, log logr.Logger) {
398403
if reconcileErr != nil {
399404
hostLease.Status.Phase = v1alpha1.HostLeasePhaseFailed
400405
hostLease.SetStatusCondition(
@@ -407,11 +412,11 @@ func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease,
407412
return
408413
}
409414

410-
if node == nil {
415+
if powerStatus == nil {
411416
return
412417
}
413418

414-
poweredOn := node.PowerState == ironic.PowerOn.String()
419+
poweredOn := powerStatus.State == management.PowerOn
415420
hostLease.Status.PoweredOn = &poweredOn
416421

417422
if hostLease.Spec.PoweredOn != nil && *hostLease.Spec.PoweredOn != poweredOn {

0 commit comments

Comments
 (0)