Skip to content

Commit b3fa6bc

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 b3fa6bc

9 files changed

Lines changed: 458 additions & 674 deletions

File tree

cmd/main.go

Lines changed: 11 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,17 @@ 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.V(1).Info("management client creation failed", "error", err)
243+
setupLog.Error(nil, "failed to create management client (check cloud credentials and endpoint configuration)")
242244
os.Exit(1)
243245
}
244-
setupLog.Info("Connect to ironic", "endpoint", ironicClient.GetEndpoint())
246+
setupLog.Info("Management client created", "type", "openstack")
245247

246248
// AAP provisioning provider for image provisioning workflows
247249
var provisioningProvider provisioning.ProvisioningProvider
@@ -272,7 +274,7 @@ func main() {
272274
hostLeaseReconciler := controller.NewHostLeaseReconciler(
273275
mgr.GetClient(),
274276
mgr.GetScheme(),
275-
ironicClient,
277+
managementClient,
276278
provisioningProvider,
277279
0, // Use DefaultRecheckInterval
278280
)

internal/controller/hostlease_controller.go

Lines changed: 55 additions & 39 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,33 +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 {
176+
if powerStatus.IsTransitioning || *hostLease.Spec.PoweredOn != (powerStatus.State == management.PowerOn) {
173177
hostLease.Status.Phase = v1alpha1.HostLeasePhaseProgressing
174178
return ctrl.Result{RequeueAfter: r.RecheckInterval}, nil
175179
}
@@ -303,38 +307,41 @@ func (r *HostLeaseReconciler) validateOpenStackHost(hostLease *v1alpha1.HostLeas
303307
return true
304308
}
305309

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()
310+
func (r *HostLeaseReconciler) reconcilePower(ctx context.Context, hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, log logr.Logger) error {
311+
currentlyOn := powerStatus.State == management.PowerOn
308312
desiredOn := *hostLease.Spec.PoweredOn
309313

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

318-
var err error
319320
needsPowerUpdate := desiredOn != currentlyOn
320321
if !needsPowerUpdate {
321-
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", node.PowerState)
322+
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", powerStatus.State)
322323
return nil
323324
}
324325

325-
targetState := ironic.PowerOff
326+
targetState := management.PowerOff
326327
action := "off"
327328
if desiredOn {
328-
targetState = ironic.PowerOn
329+
targetState = management.PowerOn
329330
action = "on"
330331
}
331332

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

337-
return err
344+
return nil
338345
}
339346

340347
func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLease *v1alpha1.HostLease) (ctrl.Result, error) {
@@ -393,8 +400,7 @@ func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLea
393400
return result, nil
394401
}
395402

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) {
403+
func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, reconcileErr error, log logr.Logger) {
398404
if reconcileErr != nil {
399405
hostLease.Status.Phase = v1alpha1.HostLeasePhaseFailed
400406
hostLease.SetStatusCondition(
@@ -407,13 +413,23 @@ func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease,
407413
return
408414
}
409415

410-
if node == nil {
416+
if powerStatus == nil {
411417
return
412418
}
413419

414-
poweredOn := node.PowerState == ironic.PowerOn.String()
420+
poweredOn := powerStatus.State == management.PowerOn
415421
hostLease.Status.PoweredOn = &poweredOn
416422

423+
if powerStatus.IsTransitioning {
424+
hostLease.SetStatusCondition(
425+
v1alpha1.HostConditionPowerSynced,
426+
metav1.ConditionFalse,
427+
v1alpha1.HostConditionReasonProgressing,
428+
"node power state is transitioning",
429+
)
430+
return
431+
}
432+
417433
if hostLease.Spec.PoweredOn != nil && *hostLease.Spec.PoweredOn != poweredOn {
418434
hostLease.SetStatusCondition(
419435
v1alpha1.HostConditionPowerSynced,

0 commit comments

Comments
 (0)