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