Skip to content

Commit a748b8e

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 3ad9e1a commit a748b8e

8 files changed

Lines changed: 415 additions & 420 deletions

File tree

cmd/main.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"context"
2021
"crypto/tls"
2122
"flag"
2223
"fmt"
2324
"os"
2425
"path/filepath"
2526
"strconv"
27+
"time"
2628

2729
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2830
// to ensure that exec-entrypoint and run can make use of them.
@@ -43,7 +45,7 @@ import (
4345

4446
v1alpha1 "github.com/osac-project/bare-metal-fulfillment-operator/api/v1alpha1"
4547
"github.com/osac-project/host-management-openstack/internal/controller"
46-
"github.com/osac-project/host-management-openstack/internal/ironic"
48+
"github.com/osac-project/host-management-openstack/internal/management"
4749
"github.com/osac-project/osac-operator/pkg/aap"
4850
"github.com/osac-project/osac-operator/pkg/provisioning"
4951
// +kubebuilder:scaffold:imports
@@ -231,13 +233,16 @@ func main() {
231233
os.Exit(1)
232234
}
233235

234-
// Ironic client for bare metal management
235-
var ironicClient *ironic.Client
236-
if ironicClient, err = ironic.NewClient(); err != nil {
237-
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")
238243
os.Exit(1)
239244
}
240-
setupLog.Info("Connect to ironic", "endpoint", ironicClient.GetEndpoint())
245+
setupLog.Info("Management client created", "type", "openstack")
241246

242247
// AAP provisioning provider for image provisioning workflows
243248
var provisioningProvider provisioning.ProvisioningProvider
@@ -268,7 +273,7 @@ func main() {
268273
hostLeaseReconciler := controller.NewHostLeaseReconciler(
269274
mgr.GetClient(),
270275
mgr.GetScheme(),
271-
ironicClient,
276+
managementClient,
272277
provisioningProvider,
273278
0, // Use DefaultRecheckInterval
274279
)

internal/controller/hostlease_controller.go

Lines changed: 33 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,
@@ -140,33 +133,32 @@ func (r *HostLeaseReconciler) handleUpdate(ctx context.Context, hostLease *v1alp
140133
}
141134
}
142135

143-
node, err := r.IronicClient.GetNode(ctx, hostLease.Spec.ExternalHostID)
136+
powerStatus, err := r.ManagementClient.GetPowerState(ctx, hostLease.Spec.ExternalHostID)
144137
if err != nil {
145-
log.Error(err, "failed to get Ironic node", "nodeID", hostLease.Spec.ExternalHostID)
138+
log.Error(err, "failed to get power state", "nodeID", hostLease.Spec.ExternalHostID)
146139
r.syncHostLeaseStatus(hostLease, nil, err, log)
147140
return ctrl.Result{}, err
148141
}
149-
log.V(1).Info("Ironic node", "nodeID", hostLease.Spec.ExternalHostID, "power_state", node.PowerState)
142+
log.V(1).Info("Host power state", "nodeID", hostLease.Spec.ExternalHostID, "power_state", powerStatus.State)
150143

151144
if hostLease.Spec.PoweredOn != nil {
152-
if err := r.reconcilePower(ctx, hostLease, node, log); err != nil {
145+
if err := r.reconcilePower(ctx, hostLease, powerStatus, log); err != nil {
153146
r.syncHostLeaseStatus(hostLease, nil, err, log)
154147
return ctrl.Result{}, err
155148
}
156149

157-
node, err = r.IronicClient.GetNode(ctx, hostLease.Spec.ExternalHostID)
150+
powerStatus, err = r.ManagementClient.GetPowerState(ctx, hostLease.Spec.ExternalHostID)
158151
if err != nil {
159-
log.Error(err, "failed to refresh node after power reconciliation", "nodeID", hostLease.Spec.ExternalHostID)
152+
log.Error(err, "failed to refresh power state after reconciliation", "nodeID", hostLease.Spec.ExternalHostID)
160153
r.syncHostLeaseStatus(hostLease, nil, err, log)
161154
return ctrl.Result{}, err
162155
}
163156
}
164157

165-
r.syncHostLeaseStatus(hostLease, node, nil, log)
158+
r.syncHostLeaseStatus(hostLease, powerStatus, nil, log)
166159

167160
if hostLease.Spec.PoweredOn != nil {
168-
currentlyOn := node.PowerState == ironic.PowerOn.String()
169-
if *hostLease.Spec.PoweredOn != currentlyOn {
161+
if *hostLease.Spec.PoweredOn != (powerStatus.State == management.PowerOn) {
170162
return ctrl.Result{RequeueAfter: r.RecheckInterval}, nil
171163
}
172164
}
@@ -292,38 +284,41 @@ func (r *HostLeaseReconciler) validateOpenStackHost(hostLease *v1alpha1.HostLeas
292284
return true
293285
}
294286

295-
func (r *HostLeaseReconciler) reconcilePower(ctx context.Context, hostLease *v1alpha1.HostLease, node *nodes.Node, log logr.Logger) error {
296-
currentlyOn := node.PowerState == ironic.PowerOn.String()
287+
func (r *HostLeaseReconciler) reconcilePower(ctx context.Context, hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, log logr.Logger) error {
288+
currentlyOn := powerStatus.State == management.PowerOn
297289
desiredOn := *hostLease.Spec.PoweredOn
298290

299-
// If Ironic is already processing a power state change, skip to avoid 409 Conflict.
300-
if r.IronicClient.IsNodePowerTransitioning(node) {
291+
if powerStatus.IsTransitioning {
301292
log.V(1).Info("Node is transitioning, skipping power action",
302-
"nodeID", hostLease.Spec.ExternalHostID,
303-
"targetPowerState", node.TargetPowerState)
293+
"nodeID", hostLease.Spec.ExternalHostID)
304294
return nil
305295
}
306296

307-
var err error
308297
needsPowerUpdate := desiredOn != currentlyOn
309298
if !needsPowerUpdate {
310-
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", node.PowerState)
299+
log.Info("Power state already matches desired", "poweredOn", desiredOn, "power_state", powerStatus.State)
311300
return nil
312301
}
313302

314-
targetState := ironic.PowerOff
303+
targetState := management.PowerOff
315304
action := "off"
316305
if desiredOn {
317-
targetState = ironic.PowerOn
306+
targetState = management.PowerOn
318307
action = "on"
319308
}
320309

321310
log.Info("Powering "+action+" node", "nodeID", hostLease.Spec.ExternalHostID)
322-
if err = r.IronicClient.SetPowerState(ctx, hostLease.Spec.ExternalHostID, targetState); err != nil {
311+
if err := r.ManagementClient.SetPowerState(ctx, hostLease.Spec.ExternalHostID, targetState); err != nil {
312+
if errors.Is(err, management.ErrTransitioning) {
313+
log.Info("Node is transitioning (conflict), will retry",
314+
"nodeID", hostLease.Spec.ExternalHostID)
315+
return nil
316+
}
323317
log.Error(err, "failed to power "+action+" node", "nodeID", hostLease.Spec.ExternalHostID)
318+
return err
324319
}
325320

326-
return err
321+
return nil
327322
}
328323

329324
func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLease *v1alpha1.HostLease) (ctrl.Result, error) {
@@ -382,8 +377,7 @@ func (r *HostLeaseReconciler) reconcileProvisioning(ctx context.Context, hostLea
382377
return result, nil
383378
}
384379

385-
// syncHostLeaseStatus syncs power-related conditions and observed power state in memory.
386-
func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease, node *nodes.Node, reconcileErr error, log logr.Logger) {
380+
func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease, powerStatus *management.PowerStatus, reconcileErr error, log logr.Logger) {
387381
if reconcileErr != nil {
388382
hostLease.Status.Phase = v1alpha1.HostLeasePhaseFailed
389383
hostLease.SetStatusCondition(
@@ -396,11 +390,11 @@ func (r *HostLeaseReconciler) syncHostLeaseStatus(hostLease *v1alpha1.HostLease,
396390
return
397391
}
398392

399-
if node == nil {
393+
if powerStatus == nil {
400394
return
401395
}
402396

403-
poweredOn := node.PowerState == ironic.PowerOn.String()
397+
poweredOn := powerStatus.State == management.PowerOn
404398
hostLease.Status.PoweredOn = &poweredOn
405399

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

0 commit comments

Comments
 (0)