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  }