github.com/sacloud/libsacloud/v2@v2.32.3/helper/builder/server/builder.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 server
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"reflect"
    22  
    23  	"github.com/sacloud/libsacloud/v2/helper/builder"
    24  	"github.com/sacloud/libsacloud/v2/helper/builder/disk"
    25  	"github.com/sacloud/libsacloud/v2/helper/plans"
    26  	"github.com/sacloud/libsacloud/v2/helper/power"
    27  	"github.com/sacloud/libsacloud/v2/helper/query"
    28  	"github.com/sacloud/libsacloud/v2/pkg/size"
    29  	"github.com/sacloud/libsacloud/v2/sacloud"
    30  	"github.com/sacloud/libsacloud/v2/sacloud/types"
    31  )
    32  
    33  // Builder サーバ作成時のパラメータ
    34  type Builder struct {
    35  	Name            string
    36  	CPU             int
    37  	MemoryGB        int
    38  	GPU             int
    39  	Commitment      types.ECommitment
    40  	Generation      types.EPlanGeneration
    41  	InterfaceDriver types.EInterfaceDriver
    42  	Description     string
    43  	IconID          types.ID
    44  	Tags            types.Tags
    45  	BootAfterCreate bool
    46  	CDROMID         types.ID
    47  	PrivateHostID   types.ID
    48  	NIC             NICSettingHolder
    49  	AdditionalNICs  []AdditionalNICSettingHolder
    50  	DiskBuilders    []disk.Builder
    51  
    52  	UserData string
    53  
    54  	Client *APIClient
    55  
    56  	NoWait bool
    57  
    58  	ServerID      types.ID
    59  	ForceShutdown bool
    60  }
    61  
    62  func BuilderFromResource(ctx context.Context, caller sacloud.APICaller, zone string, id types.ID) (*Builder, error) {
    63  	serverOp := sacloud.NewServerOp(caller)
    64  	current, err := serverOp.Read(ctx, zone, id)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	var nic NICSettingHolder
    70  	if len(current.Interfaces) > 0 {
    71  		iface := current.Interfaces[0]
    72  		switch {
    73  		case iface.SwitchID.IsEmpty():
    74  			nic = &DisconnectedNICSetting{}
    75  		case iface.SwitchScope == types.Scopes.Shared:
    76  			nic = &SharedNICSetting{PacketFilterID: iface.PacketFilterID}
    77  		default:
    78  			nic = &ConnectedNICSetting{
    79  				SwitchID:         iface.SwitchID,
    80  				DisplayIPAddress: iface.UserIPAddress,
    81  				PacketFilterID:   iface.PacketFilterID,
    82  			}
    83  		}
    84  	}
    85  
    86  	var additionalNICs []AdditionalNICSettingHolder
    87  	if len(current.Interfaces) > 1 {
    88  		for i, iface := range current.Interfaces {
    89  			if i == 0 {
    90  				continue
    91  			}
    92  			switch {
    93  			case iface.SwitchID.IsEmpty():
    94  				additionalNICs = append(additionalNICs, &DisconnectedNICSetting{})
    95  			default:
    96  				additionalNICs = append(additionalNICs, &ConnectedNICSetting{
    97  					SwitchID:         iface.SwitchID,
    98  					DisplayIPAddress: iface.UserIPAddress,
    99  					PacketFilterID:   iface.PacketFilterID,
   100  				})
   101  			}
   102  		}
   103  	}
   104  
   105  	diskOp := sacloud.NewDiskOp(caller)
   106  	var diskBuilders []disk.Builder
   107  	for _, d := range current.Disks {
   108  		currentDisk, err := diskOp.Read(ctx, zone, d.ID)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  		diskBuilders = append(diskBuilders, &disk.ConnectedDiskBuilder{
   113  			ID:            currentDisk.ID,
   114  			EditParameter: nil,
   115  			Name:          currentDisk.Name,
   116  			Description:   currentDisk.Description,
   117  			Tags:          currentDisk.Tags,
   118  			IconID:        currentDisk.IconID,
   119  			Connection:    currentDisk.Connection,
   120  			NoWait:        false,
   121  			Client:        disk.NewBuildersAPIClient(caller),
   122  		})
   123  	}
   124  
   125  	return &Builder{
   126  		Name:            current.Name,
   127  		CPU:             current.CPU,
   128  		MemoryGB:        current.MemoryMB * size.GiB,
   129  		GPU:             current.GPU,
   130  		Commitment:      current.ServerPlanCommitment,
   131  		Generation:      current.ServerPlanGeneration,
   132  		InterfaceDriver: current.InterfaceDriver,
   133  		Description:     current.Description,
   134  		IconID:          current.IconID,
   135  		Tags:            current.Tags,
   136  		BootAfterCreate: false,
   137  		CDROMID:         current.CDROMID,
   138  		PrivateHostID:   current.PrivateHostID,
   139  		NIC:             nic,
   140  		AdditionalNICs:  additionalNICs,
   141  		DiskBuilders:    diskBuilders,
   142  		Client:          NewBuildersAPIClient(caller),
   143  		ServerID:        current.ID,
   144  	}, nil
   145  }
   146  
   147  // BuildResult サーバ構築結果
   148  type BuildResult struct {
   149  	ServerID               types.ID
   150  	DiskIDs                []types.ID
   151  	GeneratedSSHPrivateKey string
   152  }
   153  
   154  var (
   155  	defaultCPU             = 1
   156  	defaultMemoryGB        = 1
   157  	defaultGPU             = 0
   158  	defaultCommitment      = types.Commitments.Standard
   159  	defaultGeneration      = types.PlanGenerations.Default
   160  	defaultInterfaceDriver = types.InterfaceDrivers.VirtIO
   161  )
   162  
   163  // Validate 入力値の検証
   164  //
   165  // 各種IDの存在確認のためにAPIリクエストが行われます。
   166  func (b *Builder) Validate(ctx context.Context, zone string) error {
   167  	b.setDefaults()
   168  
   169  	// Fields
   170  	if b.Client == nil {
   171  		return errors.New("client is empty")
   172  	}
   173  
   174  	if b.NIC == nil && len(b.AdditionalNICs) > 0 {
   175  		return errors.New("NIC is required when AdditionalNICs is specified")
   176  	}
   177  
   178  	if len(b.AdditionalNICs) > 9 {
   179  		return errors.New("AdditionalNICs must be less than 9")
   180  	}
   181  
   182  	if b.InterfaceDriver != types.InterfaceDrivers.E1000 && b.InterfaceDriver != types.InterfaceDrivers.VirtIO {
   183  		return fmt.Errorf("invalid InterfaceDriver: %s", b.InterfaceDriver)
   184  	}
   185  
   186  	// NICs
   187  	if b.NIC != nil {
   188  		if err := b.NIC.Validate(ctx, b.Client, zone); err != nil {
   189  			return fmt.Errorf("invalid NIC: %s", err)
   190  		}
   191  	}
   192  	for i, nic := range b.AdditionalNICs {
   193  		if err := nic.Validate(ctx, b.Client, zone); err != nil {
   194  			return fmt.Errorf("invalid AdditionalNICs[%d]: %s", i, err)
   195  		}
   196  	}
   197  
   198  	// server plan
   199  	_, err := query.FindServerPlan(ctx, b.Client.ServerPlan, zone, &query.FindServerPlanRequest{
   200  		CPU:        b.CPU,
   201  		MemoryGB:   b.MemoryGB,
   202  		GPU:        b.GPU,
   203  		Commitment: b.Commitment,
   204  		Generation: b.Generation,
   205  	})
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	for _, diskBuilder := range b.DiskBuilders {
   211  		if err := diskBuilder.Validate(ctx, zone); err != nil {
   212  			return err
   213  		}
   214  		if b.NoWait && !diskBuilder.NoWaitFlag() {
   215  			return errors.New("NoWait=true is not supported if the disks contain NoWait=false")
   216  		}
   217  	}
   218  
   219  	if b.NoWait && b.BootAfterCreate {
   220  		return errors.New("NoWait=true is not supported with BootAfterCreate=true")
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  // Build サーバ構築を行う
   227  func (b *Builder) Build(ctx context.Context, zone string) (*BuildResult, error) {
   228  	// validate
   229  	if err := b.Validate(ctx, zone); err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	// create server
   234  	server, err := b.createServer(ctx, zone)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	result := &BuildResult{
   239  		ServerID: server.ID,
   240  	}
   241  
   242  	// create&connect disk(s)
   243  	for _, diskReq := range b.DiskBuilders {
   244  		builtDisk, err := diskReq.Build(ctx, zone, server.ID)
   245  		if err != nil {
   246  			return result, err
   247  		}
   248  		result.DiskIDs = append(result.DiskIDs, builtDisk.DiskID)
   249  		if builtDisk.GeneratedSSHKey != nil {
   250  			result.GeneratedSSHPrivateKey = builtDisk.GeneratedSSHKey.PrivateKey
   251  		}
   252  	}
   253  
   254  	// connect packet filter
   255  	if err := b.updateInterfaces(ctx, zone, server); err != nil {
   256  		return result, err
   257  	}
   258  
   259  	// insert CD-ROM
   260  	if !b.CDROMID.IsEmpty() {
   261  		req := &sacloud.InsertCDROMRequest{ID: b.CDROMID}
   262  		if err := b.Client.Server.InsertCDROM(ctx, zone, server.ID, req); err != nil {
   263  			return result, err
   264  		}
   265  	}
   266  
   267  	// bool
   268  	if !b.NoWait && b.BootAfterCreate {
   269  		if err := power.BootServer(ctx, b.Client.Server, zone, server.ID, b.userData()...); err != nil {
   270  			return result, err
   271  		}
   272  	}
   273  
   274  	b.ServerID = result.ServerID
   275  	return result, nil
   276  }
   277  
   278  // IsNeedShutdown Update時にシャットダウンが必要か
   279  func (b *Builder) IsNeedShutdown(ctx context.Context, zone string) (bool, error) {
   280  	if b.ServerID.IsEmpty() {
   281  		return false, fmt.Errorf("server id required")
   282  	}
   283  
   284  	server, err := b.Client.Server.Read(ctx, zone, b.ServerID)
   285  	if err != nil {
   286  		return false, err
   287  	}
   288  
   289  	if b.UserData != "" {
   290  		return true, nil
   291  	}
   292  
   293  	current := b.currentState(server)
   294  	desired := b.desiredState()
   295  
   296  	// シャットダウンが不要な項目には固定値を入れる
   297  	var nics []*nicState
   298  	nics = append(nics, current.nic)
   299  	nics = append(nics, current.additionalNICs...)
   300  	nics = append(nics, desired.nic)
   301  	nics = append(nics, desired.additionalNICs...)
   302  	b.fillDummyValueToState(nics...)
   303  
   304  	if !reflect.DeepEqual(current, desired) {
   305  		return true, nil
   306  	}
   307  
   308  	// ここに到達するときはserver.Disksとb.DiskBuildersは同数となっている
   309  	for i, disk := range server.Disks {
   310  		level := b.DiskBuilders[i].UpdateLevel(ctx, zone, &sacloud.Disk{
   311  			ID:              disk.ID,
   312  			Name:            disk.Name,
   313  			Availability:    disk.Availability,
   314  			Connection:      disk.Connection,
   315  			ConnectionOrder: disk.ConnectionOrder,
   316  			ReinstallCount:  disk.ReinstallCount,
   317  			SizeMB:          disk.SizeMB,
   318  			DiskPlanID:      disk.DiskPlanID,
   319  			Storage:         disk.Storage,
   320  		})
   321  
   322  		if level == builder.UpdateLevelNeedShutdown {
   323  			return true, nil
   324  		}
   325  	}
   326  	return false, nil
   327  }
   328  
   329  func (b *Builder) fillDummyValueToState(state ...*nicState) {
   330  	for _, s := range state {
   331  		if s != nil {
   332  			s.packetFilterID = types.ID(0)
   333  			s.displayIP = ""
   334  		}
   335  	}
   336  }
   337  
   338  // Update サーバの更新
   339  func (b *Builder) Update(ctx context.Context, zone string) (*BuildResult, error) {
   340  	// validate
   341  	if err := b.Validate(ctx, zone); err != nil {
   342  		return nil, err
   343  	}
   344  	if b.ServerID.IsEmpty() {
   345  		return nil, fmt.Errorf("server id required")
   346  	}
   347  
   348  	result := &BuildResult{ServerID: b.ServerID}
   349  
   350  	server, err := b.Client.Server.Read(ctx, zone, b.ServerID)
   351  	if err != nil {
   352  		return result, err
   353  	}
   354  
   355  	isNeedShutdown, err := b.IsNeedShutdown(ctx, zone)
   356  	if err != nil {
   357  		return result, err
   358  	}
   359  
   360  	// shutdown
   361  	running := server.InstanceStatus.IsUp()
   362  	if isNeedShutdown && running {
   363  		if b.NoWait {
   364  			return nil, errors.New("NoWait option is not available due to the need to shut down")
   365  		}
   366  		if err := power.ShutdownServer(ctx, b.Client.Server, zone, server.ID, b.ForceShutdown); err != nil {
   367  			return result, err
   368  		}
   369  	}
   370  
   371  	// reconcile disks
   372  	if err := b.reconcileDisks(ctx, zone, server, result); err != nil {
   373  		return result, err
   374  	}
   375  
   376  	// reconcile interface
   377  	if err := b.reconcileInterfaces(ctx, zone, server); err != nil {
   378  		return result, err
   379  	}
   380  
   381  	// plan
   382  	if b.isPlanChanged(server) {
   383  		b.Tags = plans.AppendPreviousIDTagIfAbsent(b.Tags, server.ID)
   384  		updated, err := b.Client.Server.ChangePlan(ctx, zone, server.ID, &sacloud.ServerChangePlanRequest{
   385  			CPU:                  b.CPU,
   386  			MemoryMB:             b.MemoryGB * size.GiB,
   387  			GPU:                  b.GPU,
   388  			ServerPlanGeneration: b.Generation,
   389  			ServerPlanCommitment: b.Commitment,
   390  		})
   391  		if err != nil {
   392  			return result, err
   393  		}
   394  		server = updated
   395  	}
   396  
   397  	// update
   398  	updated, err := b.Client.Server.Update(ctx, zone, server.ID, &sacloud.ServerUpdateRequest{
   399  		Name:            b.Name,
   400  		Description:     b.Description,
   401  		Tags:            b.Tags,
   402  		IconID:          b.IconID,
   403  		PrivateHostID:   b.PrivateHostID,
   404  		InterfaceDriver: b.InterfaceDriver,
   405  	})
   406  	if err != nil {
   407  		return result, err
   408  	}
   409  	server = updated
   410  	result.ServerID = server.ID
   411  
   412  	// insert CD-ROM
   413  	if !b.CDROMID.IsEmpty() && b.CDROMID != server.CDROMID {
   414  		if !server.CDROMID.IsEmpty() {
   415  			if err := b.Client.Server.EjectCDROM(ctx, zone, server.ID, &sacloud.EjectCDROMRequest{ID: server.CDROMID}); err != nil {
   416  				return result, err
   417  			}
   418  		}
   419  		if err := b.Client.Server.InsertCDROM(ctx, zone, server.ID, &sacloud.InsertCDROMRequest{ID: b.CDROMID}); err != nil {
   420  			return result, err
   421  		}
   422  	}
   423  
   424  	// boot
   425  	if isNeedShutdown && running && server.InstanceStatus.IsDown() {
   426  		if err := power.BootServer(ctx, b.Client.Server, zone, server.ID, b.userData()...); err != nil {
   427  			return result, err
   428  		}
   429  	}
   430  
   431  	result.ServerID = server.ID
   432  	return result, nil
   433  }
   434  
   435  func (b *Builder) setDefaults() {
   436  	if b.CPU == 0 {
   437  		b.CPU = defaultCPU
   438  	}
   439  	if b.MemoryGB == 0 {
   440  		b.MemoryGB = defaultMemoryGB
   441  	}
   442  	if b.GPU == 0 {
   443  		b.GPU = defaultGPU
   444  	}
   445  	if b.Commitment == types.ECommitment("") {
   446  		b.Commitment = defaultCommitment
   447  	}
   448  	if b.Generation == types.EPlanGeneration(0) {
   449  		b.Generation = defaultGeneration
   450  	}
   451  	if b.InterfaceDriver == types.EInterfaceDriver("") {
   452  		b.InterfaceDriver = defaultInterfaceDriver
   453  	}
   454  }
   455  
   456  type serverState struct {
   457  	privateHostID   types.ID
   458  	interfaceDriver types.EInterfaceDriver
   459  	memoryGB        int
   460  	cpu             int
   461  	gpu             int
   462  	commitment      types.ECommitment
   463  	nic             *nicState   // hash
   464  	additionalNICs  []*nicState // hash
   465  	diskCount       int
   466  }
   467  
   468  func (b *Builder) desiredState() *serverState {
   469  	var nic *nicState
   470  	if b.NIC != nil {
   471  		nic = b.NIC.state()
   472  	}
   473  	var additionalNICs []*nicState
   474  	for _, n := range b.AdditionalNICs {
   475  		additionalNICs = append(additionalNICs, n.state())
   476  	}
   477  
   478  	return &serverState{
   479  		privateHostID:   b.PrivateHostID,
   480  		interfaceDriver: b.InterfaceDriver,
   481  		memoryGB:        b.MemoryGB,
   482  		cpu:             b.CPU,
   483  		gpu:             b.GPU,
   484  		commitment:      b.Commitment,
   485  		nic:             nic,
   486  		additionalNICs:  additionalNICs,
   487  		diskCount:       len(b.DiskBuilders),
   488  	}
   489  }
   490  
   491  func (b *Builder) currentNICState(nic *sacloud.InterfaceView) *nicState {
   492  	var state *nicState
   493  
   494  	switch {
   495  	case nic.SwitchScope == types.Scopes.Shared:
   496  		state = &nicState{
   497  			upstreamType:   types.UpstreamNetworkTypes.Shared,
   498  			switchID:       types.ID(0),
   499  			packetFilterID: nic.PacketFilterID,
   500  			displayIP:      "",
   501  		}
   502  	case nic.SwitchID.IsEmpty():
   503  		state = &nicState{
   504  			upstreamType:   types.UpstreamNetworkTypes.None,
   505  			switchID:       types.ID(0),
   506  			packetFilterID: types.ID(0),
   507  			displayIP:      "",
   508  		}
   509  	default:
   510  		state = &nicState{
   511  			upstreamType:   types.UpstreamNetworkTypes.Switch,
   512  			switchID:       nic.SwitchID,
   513  			packetFilterID: nic.PacketFilterID,
   514  			displayIP:      nic.UserIPAddress,
   515  		}
   516  	}
   517  	return state
   518  }
   519  
   520  func (b *Builder) currentState(server *sacloud.Server) *serverState {
   521  	var nic *nicState
   522  	var additionalNICs []*nicState
   523  	for i, n := range server.Interfaces {
   524  		state := b.currentNICState(n)
   525  		if i == 0 {
   526  			nic = state
   527  		} else {
   528  			additionalNICs = append(additionalNICs, state)
   529  		}
   530  	}
   531  
   532  	return &serverState{
   533  		privateHostID:   server.PrivateHostID,
   534  		interfaceDriver: server.InterfaceDriver,
   535  		memoryGB:        server.GetMemoryGB(),
   536  		cpu:             server.CPU,
   537  		gpu:             server.GPU,
   538  		commitment:      server.ServerPlanCommitment,
   539  		nic:             nic,
   540  		additionalNICs:  additionalNICs,
   541  		diskCount:       len(server.Disks),
   542  	}
   543  }
   544  
   545  // createServer サーバ作成
   546  func (b *Builder) createServer(ctx context.Context, zone string) (*sacloud.Server, error) {
   547  	param := &sacloud.ServerCreateRequest{
   548  		CPU:                  b.CPU,
   549  		MemoryMB:             b.MemoryGB * size.GiB,
   550  		GPU:                  b.GPU,
   551  		ServerPlanCommitment: b.Commitment,
   552  		ServerPlanGeneration: b.Generation,
   553  		InterfaceDriver:      b.InterfaceDriver,
   554  		Name:                 b.Name,
   555  		Description:          b.Description,
   556  		Tags:                 b.Tags,
   557  		IconID:               b.IconID,
   558  		WaitDiskMigration:    false,
   559  		PrivateHostID:        b.PrivateHostID,
   560  		ConnectedSwitches:    []*sacloud.ConnectedSwitch{},
   561  	}
   562  	if b.NIC != nil {
   563  		cs := b.NIC.GetConnectedSwitchParam()
   564  		if cs == nil {
   565  			param.ConnectedSwitches = append(param.ConnectedSwitches, nil)
   566  		} else {
   567  			param.ConnectedSwitches = append(param.ConnectedSwitches, cs)
   568  		}
   569  	}
   570  	if len(b.AdditionalNICs) > 0 {
   571  		for _, nic := range b.AdditionalNICs {
   572  			switchID := nic.GetSwitchID()
   573  			if switchID.IsEmpty() {
   574  				param.ConnectedSwitches = append(param.ConnectedSwitches, nil)
   575  			} else {
   576  				param.ConnectedSwitches = append(param.ConnectedSwitches, &sacloud.ConnectedSwitch{ID: switchID})
   577  			}
   578  		}
   579  	}
   580  	return b.Client.Server.Create(ctx, zone, param)
   581  }
   582  
   583  type updateInterfaceRequest struct {
   584  	index          int
   585  	packetFilterID types.ID
   586  	displayIP      string
   587  }
   588  
   589  func (b *Builder) collectInterfaceParameters() []*updateInterfaceRequest {
   590  	var reqs []*updateInterfaceRequest
   591  	if b.NIC != nil {
   592  		reqs = append(reqs, &updateInterfaceRequest{
   593  			index:          0,
   594  			packetFilterID: b.NIC.GetPacketFilterID(),
   595  			displayIP:      b.NIC.GetDisplayIPAddress(),
   596  		})
   597  	}
   598  	for i, nic := range b.AdditionalNICs {
   599  		reqs = append(reqs, &updateInterfaceRequest{
   600  			index:          i + 1,
   601  			packetFilterID: nic.GetPacketFilterID(),
   602  			displayIP:      nic.GetDisplayIPAddress(),
   603  		})
   604  	}
   605  	return reqs
   606  }
   607  
   608  func (b *Builder) updateInterfaces(ctx context.Context, zone string, server *sacloud.Server) error {
   609  	requests := b.collectInterfaceParameters()
   610  	for _, req := range requests {
   611  		if req.index < len(server.Interfaces) {
   612  			iface := server.Interfaces[req.index]
   613  
   614  			if !req.packetFilterID.IsEmpty() {
   615  				if err := b.Client.Interface.ConnectToPacketFilter(ctx, zone, iface.ID, req.packetFilterID); err != nil {
   616  					return err
   617  				}
   618  			}
   619  
   620  			if req.displayIP != "" {
   621  				if _, err := b.Client.Interface.Update(ctx, zone, iface.ID, &sacloud.InterfaceUpdateRequest{
   622  					UserIPAddress: req.displayIP,
   623  				}); err != nil {
   624  					return err
   625  				}
   626  			}
   627  		}
   628  	}
   629  	return nil
   630  }
   631  
   632  func (b *Builder) reconcileDisks(ctx context.Context, zone string, server *sacloud.Server, result *BuildResult) error {
   633  	// reconcile disks
   634  	isDiskUpdated := len(server.Disks) != len(b.DiskBuilders) // isDiskUpdateがtrueの場合、後でディスクの取外&接続を行う
   635  	for i, diskReq := range b.DiskBuilders {
   636  		if diskReq.DiskID().IsEmpty() {
   637  			res, err := diskReq.Build(ctx, zone, server.ID)
   638  			if err != nil {
   639  				return err
   640  			}
   641  			if res.GeneratedSSHKey != nil {
   642  				result.GeneratedSSHPrivateKey = res.GeneratedSSHKey.PrivateKey
   643  			}
   644  			isDiskUpdated = true
   645  		}
   646  		if len(server.Disks) > i {
   647  			disk := server.Disks[i]
   648  			level := diskReq.UpdateLevel(ctx, zone, &sacloud.Disk{
   649  				ID:              disk.ID,
   650  				Name:            disk.Name,
   651  				Availability:    disk.Availability,
   652  				Connection:      disk.Connection,
   653  				ConnectionOrder: disk.ConnectionOrder,
   654  				ReinstallCount:  disk.ReinstallCount,
   655  				SizeMB:          disk.SizeMB,
   656  				DiskPlanID:      disk.DiskPlanID,
   657  				Storage:         disk.Storage,
   658  			})
   659  			if level != builder.UpdateLevelNone {
   660  				_, err := diskReq.Update(ctx, zone)
   661  				if err != nil {
   662  					return err
   663  				}
   664  			}
   665  			if disk.ID != diskReq.DiskID() {
   666  				isDiskUpdated = true
   667  			}
   668  		}
   669  	}
   670  	if isDiskUpdated {
   671  		refreshed, err := b.Client.Server.Read(ctx, zone, server.ID)
   672  		if err != nil {
   673  			return err
   674  		}
   675  		server = refreshed
   676  
   677  		// disconnect all
   678  		for i := range server.Disks {
   679  			// disconnect
   680  			if err := b.Client.Disk.DisconnectFromServer(ctx, zone, server.Disks[i].ID); err != nil {
   681  				return err
   682  			}
   683  		}
   684  		// reconnect all
   685  		for _, diskReq := range b.DiskBuilders {
   686  			result.DiskIDs = []types.ID{}
   687  			if err := b.Client.Disk.ConnectToServer(ctx, zone, diskReq.DiskID(), server.ID); err != nil {
   688  				return err
   689  			}
   690  			result.DiskIDs = append(result.DiskIDs, diskReq.DiskID())
   691  		}
   692  	}
   693  	return nil
   694  }
   695  
   696  func (b *Builder) reconcileInterfaces(ctx context.Context, zone string, server *sacloud.Server) error {
   697  	desiredState := b.desiredState()
   698  	for i, nic := range server.Interfaces {
   699  		current := b.currentNICState(nic)
   700  		var desired *nicState
   701  		if i == 0 {
   702  			desired = desiredState.nic
   703  		} else {
   704  			if len(desiredState.additionalNICs) > i-1 {
   705  				desired = desiredState.additionalNICs[i-1]
   706  			}
   707  		}
   708  		if desired == nil {
   709  			// disconnect and delete
   710  			if !nic.SwitchID.IsEmpty() {
   711  				if err := b.Client.Interface.DisconnectFromSwitch(ctx, zone, nic.ID); err != nil {
   712  					return err
   713  				}
   714  			}
   715  			if err := b.Client.Interface.Delete(ctx, zone, nic.ID); err != nil {
   716  				return err
   717  			}
   718  			continue
   719  		}
   720  		if current.upstreamType != desired.upstreamType ||
   721  			current.switchID != desired.switchID {
   722  			if !nic.SwitchID.IsEmpty() {
   723  				if err := b.Client.Interface.DisconnectFromSwitch(ctx, zone, nic.ID); err != nil {
   724  					return err
   725  				}
   726  			}
   727  		}
   728  	}
   729  
   730  	desiredNICs := []*nicState{desiredState.nic}
   731  	desiredNICs = append(desiredNICs, desiredState.additionalNICs...)
   732  
   733  	for i, desired := range desiredNICs {
   734  		if desired == nil {
   735  			continue
   736  		}
   737  		var nic *sacloud.InterfaceView
   738  		if len(server.Interfaces) > i {
   739  			nic = server.Interfaces[i]
   740  		}
   741  		if nic == nil {
   742  			created, err := b.Client.Interface.Create(ctx, zone, &sacloud.InterfaceCreateRequest{
   743  				ServerID: server.ID,
   744  			})
   745  			if err != nil {
   746  				return err
   747  			}
   748  			nic = &sacloud.InterfaceView{
   749  				ID:             created.ID,
   750  				MACAddress:     created.MACAddress,
   751  				IPAddress:      created.IPAddress,
   752  				UserIPAddress:  created.UserIPAddress,
   753  				HostName:       created.HostName,
   754  				SwitchID:       created.SwitchID,
   755  				SwitchScope:    created.SwitchScope,
   756  				PacketFilterID: created.PacketFilterID,
   757  			}
   758  		}
   759  		switch desired.upstreamType {
   760  		case types.UpstreamNetworkTypes.None:
   761  			// noop
   762  		case types.UpstreamNetworkTypes.Shared:
   763  			if nic.SwitchScope != types.Scopes.Shared {
   764  				if err := b.Client.Interface.ConnectToSharedSegment(ctx, zone, nic.ID); err != nil {
   765  					return err
   766  				}
   767  			}
   768  		default:
   769  			if nic.SwitchID != desired.switchID {
   770  				if err := b.Client.Interface.ConnectToSwitch(ctx, zone, nic.ID, desired.switchID); err != nil {
   771  					return err
   772  				}
   773  			}
   774  		}
   775  		if desired.packetFilterID != nic.PacketFilterID {
   776  			if !nic.PacketFilterID.IsEmpty() {
   777  				if err := b.Client.Interface.DisconnectFromPacketFilter(ctx, zone, nic.ID); err != nil {
   778  					return err
   779  				}
   780  			}
   781  			if !desired.packetFilterID.IsEmpty() {
   782  				if err := b.Client.Interface.ConnectToPacketFilter(ctx, zone, nic.ID, desired.packetFilterID); err != nil {
   783  					return err
   784  				}
   785  			}
   786  		}
   787  		if desired.displayIP != nic.UserIPAddress {
   788  			if _, err := b.Client.Interface.Update(ctx, zone, nic.ID, &sacloud.InterfaceUpdateRequest{
   789  				UserIPAddress: desired.displayIP,
   790  			}); err != nil {
   791  				return err
   792  			}
   793  		}
   794  	}
   795  	return nil
   796  }
   797  
   798  func (b *Builder) isPlanChanged(server *sacloud.Server) bool {
   799  	return b.CPU != server.CPU ||
   800  		b.MemoryGB != server.GetMemoryGB() ||
   801  		b.GPU != server.GPU ||
   802  		b.Commitment != server.ServerPlanCommitment ||
   803  		(b.Generation != types.PlanGenerations.Default && b.Generation != server.ServerPlanGeneration)
   804  	//b.Generation != server.ServerPlanGeneration
   805  }
   806  
   807  func (b *Builder) userData() []string {
   808  	if b.UserData == "" {
   809  		return nil
   810  	}
   811  	return []string{b.UserData}
   812  }