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  }