github.com/sacloud/iaas-api-go@v1.12.0/helper/power/power.go (about) 1 // Copyright 2022-2023 The sacloud/iaas-api-go Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package power 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "net/http" 22 "sync" 23 "time" 24 25 "github.com/sacloud/iaas-api-go" 26 "github.com/sacloud/iaas-api-go/accessor" 27 "github.com/sacloud/iaas-api-go/defaults" 28 "github.com/sacloud/iaas-api-go/types" 29 ) 30 31 var ( 32 // BootRetrySpan 起動APIをコールしてからリトライするまでの待機時間 33 BootRetrySpan time.Duration 34 // ShutdownRetrySpan シャットダウンAPIをコールしてからリトライするまでの待機時間 35 ShutdownRetrySpan time.Duration 36 // InitialRequestTimeout 初回のBoot/Shutdownリクエストが受け入れられるまでのタイムアウト時間 37 InitialRequestTimeout time.Duration 38 // InitialRequestRetrySpan 初回のBoot/Shutdownリクエストをリトライする場合のリトライ間隔 39 InitialRequestRetrySpan time.Duration 40 ) 41 42 /************************************************ 43 * Server 44 ***********************************************/ 45 46 // BootServer 起動 47 // 48 // variablesが指定された場合、PUT /server/:id/powerのCloudInit用のパラメータとして渡される 49 // variablesが複数指定された場合は改行で結合される 50 func BootServer(ctx context.Context, client ServerAPI, zone string, id types.ID, variables ...string) error { 51 return boot(ctx, &serverHandler{ 52 ctx: ctx, 53 client: client, 54 zone: zone, 55 id: id, 56 variables: variables, 57 }) 58 } 59 60 // ShutdownServer シャットダウン 61 func ShutdownServer(ctx context.Context, client ServerAPI, zone string, id types.ID, force bool) error { 62 return shutdown(ctx, &serverHandler{ 63 ctx: ctx, 64 client: client, 65 zone: zone, 66 id: id, 67 }, force) 68 } 69 70 /************************************************ 71 * LoadBalancer 72 ***********************************************/ 73 74 // BootLoadBalancer 起動 75 func BootLoadBalancer(ctx context.Context, client LoadBalancerAPI, zone string, id types.ID) error { 76 return boot(ctx, &loadBalancerHandler{ 77 ctx: ctx, 78 client: client, 79 zone: zone, 80 id: id, 81 }) 82 } 83 84 // ShutdownLoadBalancer シャットダウン 85 func ShutdownLoadBalancer(ctx context.Context, client LoadBalancerAPI, zone string, id types.ID, force bool) error { 86 return shutdown(ctx, &loadBalancerHandler{ 87 ctx: ctx, 88 client: client, 89 zone: zone, 90 id: id, 91 }, force) 92 } 93 94 /************************************************ 95 * Database 96 ***********************************************/ 97 98 // BootDatabase 起動 99 func BootDatabase(ctx context.Context, client DatabaseAPI, zone string, id types.ID) error { 100 return boot(ctx, &databaseHandler{ 101 ctx: ctx, 102 client: client, 103 zone: zone, 104 id: id, 105 }) 106 } 107 108 // ShutdownDatabase シャットダウン 109 func ShutdownDatabase(ctx context.Context, client DatabaseAPI, zone string, id types.ID, force bool) error { 110 return shutdown(ctx, &databaseHandler{ 111 ctx: ctx, 112 client: client, 113 zone: zone, 114 id: id, 115 }, force) 116 } 117 118 /************************************************ 119 * VPCRouter 120 ***********************************************/ 121 122 // BootVPCRouter 起動 123 func BootVPCRouter(ctx context.Context, client VPCRouterAPI, zone string, id types.ID) error { 124 return boot(ctx, &vpcRouterHandler{ 125 ctx: ctx, 126 client: client, 127 zone: zone, 128 id: id, 129 }) 130 } 131 132 // ShutdownVPCRouter シャットダウン 133 func ShutdownVPCRouter(ctx context.Context, client VPCRouterAPI, zone string, id types.ID, force bool) error { 134 return shutdown(ctx, &vpcRouterHandler{ 135 ctx: ctx, 136 client: client, 137 zone: zone, 138 id: id, 139 }, force) 140 } 141 142 /************************************************ 143 * NFS 144 ***********************************************/ 145 146 // BootNFS 起動 147 func BootNFS(ctx context.Context, client NFSAPI, zone string, id types.ID) error { 148 return boot(ctx, &nfsHandler{ 149 ctx: ctx, 150 client: client, 151 zone: zone, 152 id: id, 153 }) 154 } 155 156 // ShutdownNFS シャットダウン 157 func ShutdownNFS(ctx context.Context, client NFSAPI, zone string, id types.ID, force bool) error { 158 return shutdown(ctx, &nfsHandler{ 159 ctx: ctx, 160 client: client, 161 zone: zone, 162 id: id, 163 }, force) 164 } 165 166 /************************************************ 167 * MobileGateway 168 ***********************************************/ 169 170 // BootMobileGateway 起動 171 func BootMobileGateway(ctx context.Context, client MobileGatewayAPI, zone string, id types.ID) error { 172 return boot(ctx, &mobileGatewayHandler{ 173 ctx: ctx, 174 client: client, 175 zone: zone, 176 id: id, 177 }) 178 } 179 180 // ShutdownMobileGateway シャットダウン 181 // 182 // HACK: forceオプションは現在指定不能になっているが、互換性維持のためにここの引数は残しておく 183 // 184 // forceは指定しても利用されない 185 func ShutdownMobileGateway(ctx context.Context, client MobileGatewayAPI, zone string, id types.ID, force bool) error { 186 return shutdown(ctx, &mobileGatewayHandler{ 187 ctx: ctx, 188 client: client, 189 zone: zone, 190 id: id, 191 }, false) 192 } 193 194 type handler interface { 195 boot() error 196 shutdown(force bool) error 197 read() (interface{}, error) 198 } 199 200 var mu sync.Mutex 201 202 func initDefaults() { 203 mu.Lock() 204 defer mu.Unlock() 205 206 if BootRetrySpan == 0 { 207 BootRetrySpan = defaults.DefaultPowerHelperBootRetrySpan 208 } 209 if ShutdownRetrySpan == 0 { 210 ShutdownRetrySpan = defaults.DefaultPowerHelperShutdownRetrySpan 211 } 212 if InitialRequestTimeout == 0 { 213 InitialRequestTimeout = defaults.DefaultPowerHelperInitialRequestTimeout 214 } 215 if InitialRequestRetrySpan == 0 { 216 InitialRequestRetrySpan = defaults.DefaultPowerHelperInitialRequestRetrySpan 217 } 218 } 219 220 func boot(ctx context.Context, h handler) error { 221 initDefaults() 222 223 // 初回リクエスト、409+still_creatingの場合は一定期間リトライする 224 if err := powerRequestWithRetry(ctx, h.boot); err != nil { 225 return err 226 } 227 228 retryTimer := time.NewTicker(BootRetrySpan) 229 defer retryTimer.Stop() 230 231 inProcess := false 232 233 waiter := iaas.WaiterForUp(h.read) 234 compCh, progressCh, errCh := waiter.WaitForStateAsync(ctx) 235 236 var state interface{} 237 238 for { 239 select { 240 case <-ctx.Done(): 241 return errors.New("canceled") 242 case <-compCh: 243 return nil 244 case s := <-progressCh: 245 state = s 246 case <-retryTimer.C: 247 if inProcess { 248 continue 249 } 250 if state != nil && state.(accessor.InstanceStatus).GetInstanceStatus().IsDown() { 251 if err := h.boot(); err != nil { 252 if err, ok := err.(iaas.APIError); ok { 253 // 初回リクエスト以降で409を受け取った場合はAPI側で受け入れ済とみなしこれ以上リトライしない 254 if err.ResponseCode() == http.StatusConflict { 255 inProcess = true 256 continue 257 } 258 } 259 return err 260 } 261 } 262 case err := <-errCh: 263 return err 264 } 265 } 266 } 267 268 func shutdown(ctx context.Context, h handler, force bool) error { 269 initDefaults() 270 271 // 初回リクエスト、409+still_creatingの場合は一定期間リトライする 272 if err := powerRequestWithRetry(ctx, func() error { return h.shutdown(force) }); err != nil { 273 return err 274 } 275 276 retryTimer := time.NewTicker(ShutdownRetrySpan) 277 defer retryTimer.Stop() 278 279 inProcess := false 280 281 waiter := iaas.WaiterForDown(h.read) 282 compCh, progressCh, errCh := waiter.WaitForStateAsync(ctx) 283 284 var state interface{} 285 286 for { 287 select { 288 case <-compCh: 289 return nil 290 case s := <-progressCh: 291 state = s 292 case <-retryTimer.C: 293 if inProcess { 294 continue 295 } 296 if state != nil && state.(accessor.InstanceStatus).GetInstanceStatus().IsUp() { 297 if err := h.shutdown(force); err != nil { 298 if err, ok := err.(iaas.APIError); ok { 299 // 初回リクエスト以降で409を受け取った場合はAPI側で受け入れ済とみなしこれ以上リトライしない 300 if err.ResponseCode() == http.StatusConflict { 301 inProcess = true 302 continue 303 } 304 } 305 return err 306 } 307 } 308 case err := <-errCh: 309 return err 310 } 311 } 312 } 313 314 func powerRequestWithRetry(ctx context.Context, fn func() error) error { 315 ctx, cancel := context.WithTimeout(ctx, InitialRequestTimeout) 316 defer cancel() 317 318 retryTimer := time.NewTicker(InitialRequestRetrySpan) 319 defer retryTimer.Stop() 320 321 for { 322 select { 323 case <-ctx.Done(): 324 err := ctx.Err() 325 if err != nil { 326 return fmt.Errorf("powerRequestWithRetry: timed out: %s", err) 327 } 328 return nil 329 case <-retryTimer.C: 330 err := fn() 331 if err != nil { 332 if iaas.IsStillCreatingError(err) { 333 continue 334 } 335 return err 336 } 337 return nil 338 } 339 } 340 }