@@ -3,13 +3,15 @@ package management
33import (
44 "context"
55 "encoding/json"
6+ "errors"
67 "fmt"
78 "net/http"
89
910 "github.com/gophercloud/gophercloud/v2"
1011 "github.com/gophercloud/gophercloud/v2/openstack"
1112 "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
1213 "github.com/gophercloud/utils/v2/openstack/clientconfig"
14+ ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
1315)
1416
1517var (
@@ -22,7 +24,8 @@ func init() {
2224}
2325
2426type OpenStackClient struct {
25- client * gophercloud.ServiceClient
27+ client * gophercloud.ServiceClient
28+ newServiceClient func (ctx context.Context ) (* gophercloud.ServiceClient , error )
2629}
2730
2831func NewOpenStackClient (ctx context.Context , cfg * Config ) (Client , error ) {
@@ -39,36 +42,97 @@ func NewOpenStackClient(ctx context.Context, cfg *Config) (Client, error) {
3942 }
4043 }
4144
42- clientOpts := clientconfig.ClientOpts {
43- Cloud : cloud .Cloud ,
44- AuthType : cloud .AuthType ,
45- AuthInfo : cloud .AuthInfo ,
46- RegionName : cloud .RegionName ,
47- EndpointType : cloud .EndpointType ,
45+ factory := func (ctx context.Context ) (* gophercloud.ServiceClient , error ) {
46+ clientOpts := clientconfig.ClientOpts {
47+ Cloud : cloud .Cloud ,
48+ AuthType : cloud .AuthType ,
49+ AuthInfo : cloud .AuthInfo ,
50+ RegionName : cloud .RegionName ,
51+ EndpointType : cloud .EndpointType ,
52+ }
53+
54+ providerClient , err := clientconfig .AuthenticatedClient (ctx , & clientOpts )
55+ if err != nil {
56+ return nil , fmt .Errorf ("failed to create authenticated client (check cloud credentials and endpoint configuration)" )
57+ }
58+
59+ ironicClient , err := openstack .NewBareMetalV1 (providerClient , gophercloud.EndpointOpts {
60+ Region : cloud .RegionName ,
61+ Availability : gophercloud .Availability (cloud .EndpointType ),
62+ })
63+ if err != nil {
64+ return nil , fmt .Errorf ("failed to create baremetal client (check endpoint configuration and region)" )
65+ }
66+
67+ return ironicClient , nil
4868 }
4969
50- providerClient , err := clientconfig . AuthenticatedClient (ctx , & clientOpts )
70+ sc , err := factory (ctx )
5171 if err != nil {
52- return nil , fmt . Errorf ( "failed to create authenticated client (check cloud credentials and endpoint configuration)" )
72+ return nil , err
5373 }
5474
55- ironicClient , err := openstack .NewBareMetalV1 (providerClient , gophercloud.EndpointOpts {
56- Region : cloud .RegionName ,
57- Availability : gophercloud .Availability (cloud .EndpointType ),
58- })
59- if err != nil {
60- return nil , fmt .Errorf ("failed to create baremetal client (check endpoint configuration and region)" )
75+ return & OpenStackClient {
76+ client : sc ,
77+ newServiceClient : factory ,
78+ }, nil
79+ }
80+
81+ func isAuthError (err error ) bool {
82+ if err == nil {
83+ return false
6184 }
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+ }
6295
63- return & OpenStackClient {client : ironicClient }, nil
96+ func (c * OpenStackClient ) 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+ return fmt .Errorf ("failed to recreate baremetal client: %w" , err )
102+ }
103+ if sc == nil {
104+ return fmt .Errorf ("failed to recreate baremetal client: nil service client" )
105+ }
106+ if sc .Endpoint == "" {
107+ return fmt .Errorf ("recreated baremetal client has no endpoint configured" )
108+ }
109+ c .client = sc
110+ log .Info ("ironic service client reconnected successfully" , "endpoint" , sc .Endpoint )
111+ return nil
64112}
65113
66114func (c * OpenStackClient ) GetPowerState (ctx context.Context , hostID string ) (* PowerStatus , error ) {
115+ log := ctrllog .FromContext (ctx )
67116 node , err := nodes .Get (ctx , c .client , hostID ).Extract ()
68117 if err != nil {
118+ if isAuthError (err ) {
119+ log .Info ("auth error on GetPowerState, attempting reconnect" , "nodeID" , hostID )
120+ if reconnErr := c .reconnect (ctx ); reconnErr != nil {
121+ return nil , fmt .Errorf ("get node %s: reconnect failed: %w" , hostID , reconnErr )
122+ }
123+ node , err = nodes .Get (ctx , c .client , hostID ).Extract ()
124+ if err != nil {
125+ return nil , fmt .Errorf ("get node %s after reconnect: %w" , hostID , err )
126+ }
127+ return nodePowerStatus (node , hostID )
128+ }
69129 return nil , fmt .Errorf ("get node %s: %w" , hostID , err )
70130 }
71131
132+ return nodePowerStatus (node , hostID )
133+ }
134+
135+ func nodePowerStatus (node * nodes.Node , hostID string ) (* PowerStatus , error ) {
72136 state := PowerState (node .PowerState )
73137 switch state {
74138 case PowerOn , PowerOff :
@@ -83,6 +147,7 @@ func (c *OpenStackClient) GetPowerState(ctx context.Context, hostID string) (*Po
83147}
84148
85149func (c * OpenStackClient ) SetPowerState (ctx context.Context , hostID string , target PowerState ) error {
150+ log := ctrllog .FromContext (ctx )
86151 switch target {
87152 case PowerOn , PowerOff :
88153 default :
@@ -93,6 +158,22 @@ func (c *OpenStackClient) SetPowerState(ctx context.Context, hostID string, targ
93158 Target : nodes .TargetPowerState (target ),
94159 })
95160 if err := res .ExtractErr (); err != nil {
161+ if isAuthError (err ) {
162+ log .Info ("auth error on SetPowerState, attempting reconnect" , "nodeID" , hostID , "target" , target )
163+ if reconnErr := c .reconnect (ctx ); reconnErr != nil {
164+ return fmt .Errorf ("failed to set power state on node %s: reconnect failed: %w" , hostID , reconnErr )
165+ }
166+ res = nodes .ChangePowerState (ctx , c .client , hostID , nodes.PowerStateOpts {
167+ Target : nodes .TargetPowerState (target ),
168+ })
169+ if err := res .ExtractErr (); err != nil {
170+ if gophercloud .ResponseCodeIs (err , http .StatusConflict ) {
171+ return fmt .Errorf ("node %s: %w" , hostID , ErrTransitioning )
172+ }
173+ return fmt .Errorf ("failed to set power state on node %s after reconnect: %w" , hostID , err )
174+ }
175+ return nil
176+ }
96177 if gophercloud .ResponseCodeIs (err , http .StatusConflict ) {
97178 return fmt .Errorf ("node %s: %w" , hostID , ErrTransitioning )
98179 }
0 commit comments