@@ -19,11 +19,14 @@ package ironic
1919
2020import (
2121 "context"
22+ "errors"
2223 "fmt"
24+ "net/http"
2325
2426 "github.com/gophercloud/gophercloud/v2"
2527 "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
2628 "github.com/gophercloud/utils/v2/openstack/clientconfig"
29+ ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
2730)
2831
2932// NodeClient is the interface for interacting with Ironic nodes.
@@ -36,7 +39,8 @@ type NodeClient interface {
3639// Client talks to Ironic over REST via gophercloud.
3740type (
3841 Client struct {
39- serviceClient * gophercloud.ServiceClient
42+ serviceClient * gophercloud.ServiceClient
43+ newServiceClient func (ctx context.Context ) (* gophercloud.ServiceClient , error )
4044 }
4145
4246 TargetPowerState struct {
@@ -56,24 +60,71 @@ var (
5660 SoftRebooting = TargetPowerState {nodes .SoftRebooting }
5761)
5862
59- // NewClient creates an Ironic client with no-auth (typical for in-cluster Ironic with no Keystone).
60- func NewClient () (* Client , error ) {
61- client , err := clientconfig .NewServiceClient (context .TODO (), "baremetal" , nil )
63+ // NewClient creates an Ironic client configured via clouds.yaml or OS_* environment variables.
64+ func NewClient (ctx context.Context ) (* Client , error ) {
65+ factory := func (ctx context.Context ) (* gophercloud.ServiceClient , error ) {
66+ return clientconfig .NewServiceClient (ctx , "baremetal" , nil )
67+ }
68+ sc , err := factory (ctx )
6269 if err != nil {
6370 return nil , fmt .Errorf ("failed to create baremetal client: %w" , err )
6471 }
65- if client .Endpoint == "" {
72+ if sc .Endpoint == "" {
6673 return nil , fmt .Errorf ("baremetal client has no endpoint configured" )
6774 }
6875 return & Client {
69- serviceClient : client ,
76+ serviceClient : sc ,
77+ newServiceClient : factory ,
7078 }, nil
7179}
7280
81+ func isAuthError (err error ) bool {
82+ if err == nil {
83+ return false
84+ }
85+ if gophercloud .ResponseCodeIs (err , http .StatusUnauthorized ) {
86+ return true
87+ }
88+ var errReauth gophercloud.ErrUnableToReauthenticate
89+ if errors .As (err , & errReauth ) {
90+ return true
91+ }
92+ var errAfterReauth gophercloud.ErrErrorAfterReauthentication
93+ return errors .As (err , & errAfterReauth )
94+ }
95+
96+ func (c * Client ) reconnect (ctx context.Context ) error {
97+ log := ctrllog .FromContext (ctx )
98+ log .Info ("recreating ironic service client after authentication failure" )
99+ sc , err := c .newServiceClient (ctx )
100+ if err != nil {
101+ log .Error (err , "failed to recreate ironic service client" )
102+ return fmt .Errorf ("failed to recreate baremetal client: %w" , err )
103+ }
104+ if sc .Endpoint == "" {
105+ return fmt .Errorf ("recreated baremetal client has no endpoint configured" )
106+ }
107+ c .serviceClient = sc
108+ log .Info ("ironic service client reconnected successfully" , "endpoint" , sc .Endpoint )
109+ return nil
110+ }
111+
73112// GetNode fetches a node by UUID or name from Ironic.
74113func (c * Client ) GetNode (ctx context.Context , nodeID string ) (* nodes.Node , error ) {
114+ log := ctrllog .FromContext (ctx )
75115 node , err := nodes .Get (ctx , c .serviceClient , nodeID ).Extract ()
76116 if err != nil {
117+ if isAuthError (err ) {
118+ log .Info ("auth error on GetNode, attempting reconnect" , "nodeID" , nodeID , "error" , err )
119+ if reconnErr := c .reconnect (ctx ); reconnErr != nil {
120+ return nil , fmt .Errorf ("get node %s: reconnect failed: %w" , nodeID , reconnErr )
121+ }
122+ node , err = nodes .Get (ctx , c .serviceClient , nodeID ).Extract ()
123+ if err != nil {
124+ return nil , fmt .Errorf ("get node %s after reconnect: %w" , nodeID , err )
125+ }
126+ return node , nil
127+ }
77128 return nil , fmt .Errorf ("get node %s: %w" , nodeID , err )
78129 }
79130 return node , nil
@@ -91,8 +142,20 @@ func (c *Client) IsNodePowerTransitioning(node *nodes.Node) bool {
91142
92143// SetPowerState requests power on or off for the node via Ironic.
93144func (c * Client ) SetPowerState (ctx context.Context , nodeID string , target TargetPowerState ) error {
145+ log := ctrllog .FromContext (ctx )
94146 res := nodes .ChangePowerState (ctx , c .serviceClient , nodeID , nodes.PowerStateOpts {Target : target .powerstate })
95147 if err := res .ExtractErr (); err != nil {
148+ if isAuthError (err ) {
149+ log .Info ("auth error on SetPowerState, attempting reconnect" , "nodeID" , nodeID , "target" , target .String (), "error" , err )
150+ if reconnErr := c .reconnect (ctx ); reconnErr != nil {
151+ return fmt .Errorf ("failed to set power state on node %s: reconnect failed: %w" , nodeID , reconnErr )
152+ }
153+ res = nodes .ChangePowerState (ctx , c .serviceClient , nodeID , nodes.PowerStateOpts {Target : target .powerstate })
154+ if err := res .ExtractErr (); err != nil {
155+ return fmt .Errorf ("failed to set power state on node %s after reconnect: %w" , nodeID , err )
156+ }
157+ return nil
158+ }
96159 return fmt .Errorf ("failed to set power state on node %s: %w" , nodeID , err )
97160 }
98161 return nil
0 commit comments