github.com/sacloud/libsacloud/v2@v2.32.3/helper/builder/server/builder_test.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  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/sacloud/libsacloud/v2/helper/api"
    25  	"github.com/sacloud/libsacloud/v2/helper/builder"
    26  	"github.com/sacloud/libsacloud/v2/helper/builder/disk"
    27  	"github.com/sacloud/libsacloud/v2/helper/plans"
    28  	"github.com/sacloud/libsacloud/v2/helper/power"
    29  	"github.com/sacloud/libsacloud/v2/sacloud"
    30  	"github.com/sacloud/libsacloud/v2/sacloud/ostype"
    31  	"github.com/sacloud/libsacloud/v2/sacloud/testutil"
    32  	"github.com/sacloud/libsacloud/v2/sacloud/types"
    33  	"github.com/stretchr/testify/require"
    34  	"golang.org/x/crypto/ssh"
    35  )
    36  
    37  func init() {
    38  	if !testutil.IsAccTest() {
    39  		api.SetupFakeDefaults()
    40  	}
    41  }
    42  
    43  func TestBuilder_setDefaults(t *testing.T) {
    44  	in := &Builder{}
    45  	in.setDefaults()
    46  
    47  	expected := &Builder{
    48  		CPU:             defaultCPU,
    49  		MemoryGB:        defaultMemoryGB,
    50  		Commitment:      defaultCommitment,
    51  		Generation:      defaultGeneration,
    52  		InterfaceDriver: defaultInterfaceDriver,
    53  	}
    54  	require.Equal(t, expected, in)
    55  }
    56  
    57  func TestBuilder_Validate(t *testing.T) {
    58  	cases := []struct {
    59  		msg string
    60  		in  *Builder
    61  		err error
    62  	}{
    63  		{
    64  			msg: "Client is not set",
    65  			in:  &Builder{},
    66  			err: errors.New("client is empty"),
    67  		},
    68  		{
    69  			msg: "invalid NICs",
    70  			in: &Builder{
    71  				NIC: nil,
    72  				AdditionalNICs: []AdditionalNICSettingHolder{
    73  					&DisconnectedNICSetting{},
    74  				},
    75  				Client: &APIClient{
    76  					ServerPlan: &dummyPlanFinder{},
    77  				},
    78  			},
    79  			err: errors.New("NIC is required when AdditionalNICs is specified"),
    80  		},
    81  		{
    82  			msg: "Additional NICs over 9",
    83  			in: &Builder{
    84  				NIC: &SharedNICSetting{},
    85  				AdditionalNICs: []AdditionalNICSettingHolder{
    86  					&DisconnectedNICSetting{},
    87  					&DisconnectedNICSetting{},
    88  					&DisconnectedNICSetting{},
    89  					&DisconnectedNICSetting{},
    90  					&DisconnectedNICSetting{},
    91  					&DisconnectedNICSetting{},
    92  					&DisconnectedNICSetting{},
    93  					&DisconnectedNICSetting{},
    94  					&DisconnectedNICSetting{},
    95  					&DisconnectedNICSetting{},
    96  				},
    97  				Client: &APIClient{
    98  					ServerPlan: &dummyPlanFinder{},
    99  				},
   100  			},
   101  			err: errors.New("AdditionalNICs must be less than 9"),
   102  		},
   103  		{
   104  			msg: "invalid InterfaceDriver",
   105  			in: &Builder{
   106  				NIC:             &SharedNICSetting{},
   107  				InterfaceDriver: types.EInterfaceDriver("invalid"),
   108  				Client: &APIClient{
   109  					ServerPlan: &dummyPlanFinder{},
   110  				},
   111  			},
   112  			err: errors.New("invalid InterfaceDriver: invalid"),
   113  		},
   114  		{
   115  			msg: "finding plan returns unexpected error",
   116  			in: &Builder{
   117  				Client: &APIClient{
   118  					ServerPlan: &dummyPlanFinder{
   119  						err: errors.New("dummy"),
   120  					},
   121  				},
   122  			},
   123  			err: errors.New("dummy"),
   124  		},
   125  		{
   126  			msg: "eth0: switch not found",
   127  			in: &Builder{
   128  				NIC: &ConnectedNICSetting{
   129  					SwitchID: 1111111,
   130  				},
   131  				Client: &APIClient{
   132  					Switch: &dummySwitchReader{
   133  						err: errors.New("not found"),
   134  					},
   135  				},
   136  			},
   137  			err: errors.New("invalid NIC: reading switch info(id:1111111) is failed: not found"),
   138  		},
   139  		{
   140  			msg: "eth1: switch not found",
   141  			in: &Builder{
   142  				NIC: &SharedNICSetting{},
   143  				AdditionalNICs: []AdditionalNICSettingHolder{
   144  					&ConnectedNICSetting{
   145  						SwitchID: 1111111,
   146  					},
   147  				},
   148  				Client: &APIClient{
   149  					Switch: &dummySwitchReader{
   150  						err: errors.New("not found"),
   151  					},
   152  				},
   153  			},
   154  			err: errors.New("invalid AdditionalNICs[0]: reading switch info(id:1111111) is failed: not found"),
   155  		},
   156  		{
   157  			msg: "plan not found",
   158  			in: &Builder{
   159  				CPU:      1000,
   160  				MemoryGB: 1024,
   161  				Client: &APIClient{
   162  					ServerPlan: &dummyPlanFinder{},
   163  				},
   164  			},
   165  			err: errors.New("server plan not found"),
   166  		},
   167  	}
   168  
   169  	for _, tc := range cases {
   170  		err := tc.in.Validate(context.Background(), "tk1v")
   171  		require.Equal(t, tc.err, err, tc.msg)
   172  	}
   173  }
   174  
   175  func TestBuilder_Build(t *testing.T) {
   176  	cases := []struct {
   177  		msg string
   178  		in  *Builder
   179  		out *BuildResult
   180  		err error
   181  	}{
   182  		{
   183  			msg: "Validate func is called",
   184  			in:  &Builder{},
   185  			out: nil,
   186  			err: errors.New("client is empty"),
   187  		},
   188  		{
   189  			msg: "finding server plan API returns error",
   190  			in: &Builder{
   191  				Client: &APIClient{
   192  					Switch:       &dummySwitchReader{},
   193  					PacketFilter: &dummyPackerFilterReader{},
   194  					ServerPlan: &dummyPlanFinder{
   195  						err: errors.New("dummy"),
   196  					},
   197  				},
   198  			},
   199  			out: nil,
   200  			err: errors.New("dummy"),
   201  		},
   202  		{
   203  			msg: "creating server returns error",
   204  			in: &Builder{
   205  				Client: &APIClient{
   206  					Switch:       &dummySwitchReader{},
   207  					PacketFilter: &dummyPackerFilterReader{},
   208  					ServerPlan: &dummyPlanFinder{
   209  						plans: []*sacloud.ServerPlan{
   210  							{
   211  								ID: 1,
   212  							},
   213  						},
   214  					},
   215  					Server: &dummyCreateServerHandler{
   216  						err: errors.New("dummy"),
   217  					},
   218  				},
   219  			},
   220  			out: nil,
   221  			err: errors.New("dummy"),
   222  		},
   223  		{
   224  			msg: "validating disk returns error",
   225  			in: &Builder{
   226  				DiskBuilders: []disk.Builder{
   227  					&dummyDiskBuilder{
   228  						err: errors.New("dummy"),
   229  					},
   230  				},
   231  				Client: &APIClient{
   232  					Switch:       &dummySwitchReader{},
   233  					PacketFilter: &dummyPackerFilterReader{},
   234  					ServerPlan: &dummyPlanFinder{
   235  						plans: []*sacloud.ServerPlan{
   236  							{
   237  								ID: 1,
   238  							},
   239  						},
   240  					},
   241  					Server: &dummyCreateServerHandler{
   242  						server: &sacloud.Server{ID: 1},
   243  					},
   244  				},
   245  			},
   246  			out: nil,
   247  			err: errors.New("dummy"),
   248  		},
   249  		{
   250  			msg: "updating NIC returns error",
   251  			in: &Builder{
   252  				NIC: &SharedNICSetting{
   253  					PacketFilterID: 2,
   254  				},
   255  				Client: &APIClient{
   256  					Switch:       &dummySwitchReader{},
   257  					PacketFilter: &dummyPackerFilterReader{},
   258  					ServerPlan: &dummyPlanFinder{
   259  						plans: []*sacloud.ServerPlan{
   260  							{
   261  								ID: 1,
   262  							},
   263  						},
   264  					},
   265  					Server: &dummyCreateServerHandler{
   266  						server: &sacloud.Server{
   267  							ID: 1,
   268  							Interfaces: []*sacloud.InterfaceView{
   269  								{ID: 1},
   270  							},
   271  						},
   272  					},
   273  					Interface: &dummyInterfaceHandler{
   274  						err: errors.New("dummy"),
   275  					},
   276  				},
   277  			},
   278  			out: &BuildResult{ServerID: 1},
   279  			err: errors.New("dummy"),
   280  		},
   281  		{
   282  			msg: "inserting CD-ROM returns error",
   283  			in: &Builder{
   284  				CDROMID: 1,
   285  				Client: &APIClient{
   286  					Switch:       &dummySwitchReader{},
   287  					PacketFilter: &dummyPackerFilterReader{},
   288  					ServerPlan: &dummyPlanFinder{
   289  						plans: []*sacloud.ServerPlan{
   290  							{
   291  								ID: 1,
   292  							},
   293  						},
   294  					},
   295  					Server: &dummyCreateServerHandler{
   296  						server:   &sacloud.Server{ID: 1},
   297  						cdromErr: errors.New("dummy"),
   298  					},
   299  				},
   300  			},
   301  			out: &BuildResult{ServerID: 1},
   302  			err: errors.New("dummy"),
   303  		},
   304  		{
   305  			msg: "booting server returns error",
   306  			in: &Builder{
   307  				BootAfterCreate: true,
   308  				Client: &APIClient{
   309  					Switch:       &dummySwitchReader{},
   310  					PacketFilter: &dummyPackerFilterReader{},
   311  					ServerPlan: &dummyPlanFinder{
   312  						plans: []*sacloud.ServerPlan{
   313  							{
   314  								ID: 1,
   315  							},
   316  						},
   317  					},
   318  					Server: &dummyCreateServerHandler{
   319  						server:  &sacloud.Server{ID: 1},
   320  						bootErr: errors.New("dummy"),
   321  					},
   322  				},
   323  			},
   324  			out: &BuildResult{ServerID: 1},
   325  			err: errors.New("dummy"),
   326  		},
   327  	}
   328  	for _, tc := range cases {
   329  		res, err := tc.in.Build(context.Background(), "tk1v")
   330  		require.Equal(t, tc.err, err, tc.msg)
   331  		require.Equal(t, tc.out, res, tc.msg)
   332  	}
   333  }
   334  
   335  type dummyDiskBuilder struct {
   336  	result       *disk.BuildResult
   337  	updateResult *disk.UpdateResult
   338  	diskID       types.ID
   339  	updateLevel  builder.UpdateLevel
   340  	noWait       bool
   341  	err          error
   342  }
   343  
   344  func (d *dummyDiskBuilder) Validate(ctx context.Context, zone string) error {
   345  	return d.err
   346  }
   347  
   348  func (d *dummyDiskBuilder) Build(ctx context.Context, zone string, serverID types.ID) (*disk.BuildResult, error) {
   349  	if d.err != nil {
   350  		return nil, d.err
   351  	}
   352  	return d.result, nil
   353  }
   354  
   355  // Update ディスクの更新
   356  func (d *dummyDiskBuilder) Update(ctx context.Context, zone string) (*disk.UpdateResult, error) {
   357  	if d.err != nil {
   358  		return nil, d.err
   359  	}
   360  	return d.updateResult, nil
   361  }
   362  
   363  func (d *dummyDiskBuilder) DiskID() types.ID {
   364  	return d.diskID
   365  }
   366  
   367  func (d *dummyDiskBuilder) UpdateLevel(ctx context.Context, zone string, disk *sacloud.Disk) builder.UpdateLevel {
   368  	return d.updateLevel
   369  }
   370  
   371  func (d *dummyDiskBuilder) NoWaitFlag() bool {
   372  	return d.noWait
   373  }
   374  
   375  func TestBuilder_Build_BlackBox(t *testing.T) {
   376  	var switchID types.ID
   377  	var diskIDs []types.ID
   378  	var blackboxBuilder *Builder
   379  	var buildResult *BuildResult
   380  	var testZone = testutil.TestZone()
   381  
   382  	testutil.RunCRUD(t, &testutil.CRUDTestCase{
   383  		SetupAPICallerFunc: func() sacloud.APICaller {
   384  			return testutil.SingletonAPICaller()
   385  		},
   386  		Parallel:          true,
   387  		IgnoreStartupWait: true,
   388  
   389  		Setup: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) error {
   390  			switchOp := sacloud.NewSwitchOp(caller)
   391  			sw, err := switchOp.Create(ctx, testZone,
   392  				&sacloud.SwitchCreateRequest{
   393  					Name: "libsacloud-switch-for-builder",
   394  				},
   395  			)
   396  			if err != nil {
   397  				return err
   398  			}
   399  			switchID = sw.ID
   400  			blackboxBuilder = getBlackBoxTestBuilder(switchID)
   401  			return nil
   402  		},
   403  
   404  		Create: &testutil.CRUDTestFunc{
   405  			Func: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) (interface{}, error) {
   406  				return blackboxBuilder.Build(ctx, testZone)
   407  			},
   408  			SkipExtractID: true,
   409  			CheckFunc: func(t testutil.TestT, ctx *testutil.CRUDTestContext, v interface{}) error {
   410  				result := v.(*BuildResult)
   411  				err := testutil.DoAsserts(
   412  					testutil.AssertNotEmptyFunc(t, result.ServerID, "BuildResult.ServerID"),
   413  					testutil.AssertNotEmptyFunc(t, result.GeneratedSSHPrivateKey, "BuildResult.GeneratedSSHPrivateKey"),
   414  				)
   415  				if err != nil {
   416  					return err
   417  				}
   418  				buildResult = result
   419  				ctx.ID = result.ServerID
   420  				return nil
   421  			},
   422  		},
   423  
   424  		Read: &testutil.CRUDTestFunc{
   425  			Func: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) (interface{}, error) {
   426  				serverOp := sacloud.NewServerOp(caller)
   427  				server, err := serverOp.Read(ctx, testZone, ctx.ID)
   428  				if err != nil {
   429  					return nil, err
   430  				}
   431  				diskIDs = []types.ID{}
   432  				for _, disk := range server.Disks {
   433  					diskIDs = append(diskIDs, disk.ID)
   434  				}
   435  				return server, nil
   436  			},
   437  			CheckFunc: func(t testutil.TestT, ctx *testutil.CRUDTestContext, i interface{}) error {
   438  				if testutil.IsAccTest() && testZone != "tk1v" { // サンドボックス以外
   439  					time.Sleep(30 * time.Second) // sshd起動まで少し待つ
   440  					server := i.(*sacloud.Server)
   441  					ip := server.Interfaces[0].IPAddress
   442  					return connectToServerViaSSH(t, "root", ip, []byte(buildResult.GeneratedSSHPrivateKey), []byte("libsacloud-test-passphrase"))
   443  				}
   444  				return nil
   445  			},
   446  			SkipExtractID: true,
   447  		},
   448  		Updates: []*testutil.CRUDTestFunc{
   449  			{
   450  				Func: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) (interface{}, error) {
   451  					blackboxBuilder.AdditionalNICs = []AdditionalNICSettingHolder{blackboxBuilder.AdditionalNICs[1]}
   452  					blackboxBuilder.DiskBuilders = append(blackboxBuilder.DiskBuilders, &disk.BlankBuilder{
   453  						Name:        "libsacloud-disk-builder",
   454  						SizeGB:      20,
   455  						PlanID:      types.DiskPlans.SSD,
   456  						Connection:  types.DiskConnections.VirtIO,
   457  						Description: "libsacloud-disk-builder-description",
   458  						Tags:        types.Tags{"tag1", "tag2"},
   459  						Client:      disk.NewBuildersAPIClient(testutil.SingletonAPICaller()),
   460  					})
   461  					return blackboxBuilder.Update(ctx, testZone)
   462  				},
   463  				SkipExtractID: true,
   464  				CheckFunc: func(t testutil.TestT, ctx *testutil.CRUDTestContext, v interface{}) error {
   465  					result := v.(*BuildResult)
   466  					buildResult = result
   467  					ctx.ID = result.ServerID
   468  					return nil
   469  				},
   470  			},
   471  		},
   472  		Shutdown: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) error {
   473  			return power.ShutdownServer(ctx, sacloud.NewServerOp(caller), testZone, ctx.ID, true)
   474  		},
   475  		Delete: &testutil.CRUDTestDeleteFunc{
   476  			Func: func(ctx *testutil.CRUDTestContext, caller sacloud.APICaller) error {
   477  				serverOp := sacloud.NewServerOp(caller)
   478  				if err := serverOp.DeleteWithDisks(ctx, testZone, ctx.ID, &sacloud.ServerDeleteWithDisksRequest{IDs: diskIDs}); err != nil {
   479  					return err
   480  				}
   481  
   482  				switchOp := sacloud.NewSwitchOp(caller)
   483  				return switchOp.Delete(ctx, testZone, switchID)
   484  			},
   485  		},
   486  	})
   487  }
   488  
   489  func getBlackBoxTestBuilder(switchID types.ID) *Builder {
   490  	return &Builder{
   491  		Name:            "libsacloud-server-builder",
   492  		CPU:             1,
   493  		MemoryGB:        1,
   494  		Description:     "libsacloud-server-builder-description",
   495  		Tags:            types.Tags{"tag1", "tag2"},
   496  		BootAfterCreate: true,
   497  		NIC:             &SharedNICSetting{},
   498  		AdditionalNICs: []AdditionalNICSettingHolder{
   499  			&DisconnectedNICSetting{},
   500  			&ConnectedNICSetting{SwitchID: switchID},
   501  		},
   502  		DiskBuilders: []disk.Builder{
   503  			&disk.FromUnixBuilder{
   504  				OSType:      ostype.CentOS,
   505  				Name:        "libsacloud-disk-builder",
   506  				SizeGB:      20,
   507  				PlanID:      types.DiskPlans.SSD,
   508  				Connection:  types.DiskConnections.VirtIO,
   509  				Description: "libsacloud-disk-builder-description",
   510  				Tags:        types.Tags{"tag1", "tag2"},
   511  				EditParameter: &disk.UnixEditRequest{
   512  					HostName:                  "libsacloud-disk-builder",
   513  					Password:                  "libsacloud-test-password",
   514  					DisablePWAuth:             true,
   515  					EnableDHCP:                false,
   516  					ChangePartitionUUID:       true,
   517  					IsSSHKeysEphemeral:        true,
   518  					GenerateSSHKeyName:        "libsacloud-sshkey-generated",
   519  					GenerateSSHKeyDescription: "libsacloud-sshkey-generated-for-builder",
   520  					GenerateSSHKeyPassPhrase:  "libsacloud-test-passphrase",
   521  					//IPAddress      string
   522  					//NetworkMaskLen int
   523  					//DefaultRoute   string
   524  					//SSHKeys   []string
   525  					//SSHKeyIDs []types.ID
   526  					IsNotesEphemeral: true,
   527  					NoteContents: []string{
   528  						`libsacloud-startup-script-for-builder`,
   529  					},
   530  					//Notes          []*sacloud.DiskEditNote{},
   531  				},
   532  				Client: disk.NewBuildersAPIClient(testutil.SingletonAPICaller()),
   533  			},
   534  		},
   535  		Client: NewBuildersAPIClient(testutil.SingletonAPICaller()),
   536  	}
   537  }
   538  
   539  func connectToServerViaSSH(t testutil.TestT, user, ip string, privateKey []byte, passPhrase []byte) error {
   540  	signer, err := ssh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
   541  	if err != nil {
   542  		return err
   543  	}
   544  
   545  	config := &ssh.ClientConfig{
   546  		User: user,
   547  		Auth: []ssh.AuthMethod{
   548  			ssh.PublicKeys(signer),
   549  		},
   550  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
   551  		Timeout:         10 * time.Second,
   552  	}
   553  
   554  	client, err := ssh.Dial("tcp", ip+":22", config)
   555  	if err != nil {
   556  		return err
   557  	}
   558  	defer client.Close()
   559  
   560  	session, err := client.NewSession()
   561  	if err != nil {
   562  		return err
   563  	}
   564  	defer session.Close()
   565  
   566  	var b bytes.Buffer
   567  	session.Stdout = &b
   568  	if err := session.Run("/usr/bin/whoami"); err != nil {
   569  		return err
   570  	}
   571  	t.Logf("Connect to the Server via SSH: `whoami`: %s\n", b.String())
   572  	return nil
   573  }
   574  
   575  func TestBuilder_IsNeedShutdown(t *testing.T) {
   576  	cases := []struct {
   577  		msg    string
   578  		in     *Builder
   579  		expect bool
   580  		err    error
   581  	}{
   582  		{
   583  			msg:    "server id is empty",
   584  			expect: false,
   585  			err:    errors.New("server id required"),
   586  			in:     &Builder{},
   587  		},
   588  		{
   589  			msg:    "in-place update",
   590  			expect: false,
   591  			err:    nil,
   592  			in: &Builder{
   593  				ServerID:    types.ID(1),
   594  				Name:        "update",
   595  				Description: "update",
   596  				Tags:        types.Tags{"update1", "update2"},
   597  				Client: &APIClient{
   598  					Server: &dummyCreateServerHandler{
   599  						server: &sacloud.Server{
   600  							ID:          types.ID(1),
   601  							Name:        "update",
   602  							Description: "update",
   603  							Tags:        types.Tags{"update1", "update2-upd"},
   604  						},
   605  					},
   606  				},
   607  			},
   608  		},
   609  		{
   610  			msg:    "changed: PrivateHostID",
   611  			expect: true,
   612  			err:    nil,
   613  			in: &Builder{
   614  				ServerID:      types.ID(1),
   615  				PrivateHostID: types.ID(2),
   616  				Client: &APIClient{
   617  					Server: &dummyCreateServerHandler{
   618  						server: &sacloud.Server{
   619  							ID:            types.ID(1),
   620  							PrivateHostID: types.ID(3),
   621  						},
   622  					},
   623  				},
   624  			},
   625  		},
   626  		{
   627  			msg:    "changed: InterfaceDriver",
   628  			expect: true,
   629  			err:    nil,
   630  			in: &Builder{
   631  				ServerID:        types.ID(1),
   632  				InterfaceDriver: types.InterfaceDrivers.E1000,
   633  				Client: &APIClient{
   634  					Server: &dummyCreateServerHandler{
   635  						server: &sacloud.Server{
   636  							ID:              types.ID(1),
   637  							InterfaceDriver: types.InterfaceDrivers.VirtIO,
   638  						},
   639  					},
   640  				},
   641  			},
   642  		},
   643  		{
   644  			msg:    "changed: Memory Size",
   645  			expect: true,
   646  			err:    nil,
   647  			in: &Builder{
   648  				ServerID: types.ID(1),
   649  				MemoryGB: 1,
   650  				Client: &APIClient{
   651  					Server: &dummyCreateServerHandler{
   652  						server: &sacloud.Server{
   653  							ID:       types.ID(1),
   654  							MemoryMB: 2,
   655  						},
   656  					},
   657  				},
   658  			},
   659  		},
   660  		{
   661  			msg:    "changed: CPU",
   662  			expect: true,
   663  			err:    nil,
   664  			in: &Builder{
   665  				ServerID: types.ID(1),
   666  				CPU:      1,
   667  				Client: &APIClient{
   668  					Server: &dummyCreateServerHandler{
   669  						server: &sacloud.Server{
   670  							ID:  types.ID(1),
   671  							CPU: 2,
   672  						},
   673  					},
   674  				},
   675  			},
   676  		},
   677  		{
   678  			msg:    "changed: Commitment",
   679  			expect: true,
   680  			err:    nil,
   681  			in: &Builder{
   682  				ServerID: types.ID(1),
   683  				CPU:      1,
   684  				Client: &APIClient{
   685  					Server: &dummyCreateServerHandler{
   686  						server: &sacloud.Server{
   687  							ID:                   types.ID(1),
   688  							CPU:                  1,
   689  							ServerPlanCommitment: types.Commitments.DedicatedCPU,
   690  						},
   691  					},
   692  				},
   693  			},
   694  		},
   695  		{
   696  			msg:    "changed: add NIC",
   697  			expect: true,
   698  			err:    nil,
   699  			in: &Builder{
   700  				ServerID: types.ID(1),
   701  				NIC:      &SharedNICSetting{},
   702  				Client: &APIClient{
   703  					Server: &dummyCreateServerHandler{
   704  						server: &sacloud.Server{
   705  							ID: types.ID(1),
   706  						},
   707  					},
   708  				},
   709  			},
   710  		},
   711  		{
   712  			msg:    "changed: delete NIC",
   713  			expect: true,
   714  			err:    nil,
   715  			in: &Builder{
   716  				ServerID: types.ID(1),
   717  				Client: &APIClient{
   718  					Server: &dummyCreateServerHandler{
   719  						server: &sacloud.Server{
   720  							ID: types.ID(1),
   721  							Interfaces: []*sacloud.InterfaceView{
   722  								{
   723  									ID:             types.ID(2),
   724  									SwitchScope:    types.Scopes.Shared,
   725  									PacketFilterID: 0,
   726  									UpstreamType:   types.UpstreamNetworkTypes.Shared,
   727  								},
   728  							},
   729  						},
   730  					},
   731  				},
   732  			},
   733  		},
   734  		{
   735  			msg:    "changed: Packet Filter ID",
   736  			expect: false,
   737  			err:    nil,
   738  			in: &Builder{
   739  				ServerID: types.ID(1),
   740  				NIC: &SharedNICSetting{
   741  					PacketFilterID: types.ID(10),
   742  				},
   743  				Client: &APIClient{
   744  					Server: &dummyCreateServerHandler{
   745  						server: &sacloud.Server{
   746  							ID: types.ID(1),
   747  							Interfaces: []*sacloud.InterfaceView{
   748  								{
   749  									ID:             types.ID(2),
   750  									SwitchScope:    types.Scopes.Shared,
   751  									PacketFilterID: types.ID(11),
   752  									UpstreamType:   types.UpstreamNetworkTypes.Shared,
   753  								},
   754  							},
   755  						},
   756  					},
   757  				},
   758  			},
   759  		},
   760  	}
   761  
   762  	for _, tc := range cases {
   763  		got, err := tc.in.IsNeedShutdown(context.Background(), testutil.TestZone())
   764  		require.Equal(t, tc.err, err, tc.msg)
   765  		require.Equal(t, tc.expect, got, tc.msg)
   766  	}
   767  }
   768  
   769  func TestBuilder_UpdateWithPreviousID(t *testing.T) {
   770  	ctx := context.Background()
   771  	builder := &Builder{
   772  		Name:            testutil.ResourceName("server-builder"),
   773  		CPU:             1,
   774  		MemoryGB:        1,
   775  		Commitment:      types.Commitments.Standard,
   776  		Generation:      types.PlanGenerations.Default,
   777  		Tags:            types.Tags{"tag1", "tag2"},
   778  		BootAfterCreate: false,
   779  		Client:          NewBuildersAPIClient(testutil.SingletonAPICaller()),
   780  		ForceShutdown:   true,
   781  	}
   782  	createResult, err := builder.Build(ctx, testutil.TestZone())
   783  	if err != nil {
   784  		t.Fatal(err)
   785  	}
   786  
   787  	serverOp := sacloud.NewServerOp(testutil.SingletonAPICaller())
   788  	server, err := serverOp.Read(ctx, testutil.TestZone(), createResult.ServerID)
   789  	if err != nil {
   790  		t.Fatal(err)
   791  	}
   792  
   793  	require.EqualValues(t, builder.Tags, server.Tags)
   794  
   795  	// プラン変更
   796  	builder.ServerID = server.ID
   797  	builder.CPU = 2
   798  	builder.MemoryGB = 4
   799  
   800  	updateResult, err := builder.Update(ctx, testutil.TestZone())
   801  	if err != nil {
   802  		t.Fatal(err)
   803  	}
   804  
   805  	// IDが変更されているはず
   806  	require.True(t, createResult.ServerID != updateResult.ServerID)
   807  	updated, err := serverOp.Read(ctx, testutil.TestZone(), updateResult.ServerID)
   808  	if err != nil {
   809  		t.Fatal(err)
   810  	}
   811  
   812  	require.EqualValues(t, plans.AppendPreviousIDTagIfAbsent(server.Tags, server.ID), updated.Tags)
   813  
   814  	// cleanup
   815  	if err := serverOp.Delete(ctx, testutil.TestZone(), updated.ID); err != nil {
   816  		t.Fatal(err)
   817  	}
   818  }
   819  
   820  func TestBuilder_GPUPlan(t *testing.T) {
   821  	if !testutil.IsAccTest() {
   822  		t.Skip("TestBuilder_GPUPlan only exec when running an Acceptance Test")
   823  	}
   824  
   825  	ctx := context.Background()
   826  	zone := "is1a"
   827  	builder := &Builder{
   828  		Name:            testutil.ResourceName("server-builder"),
   829  		CPU:             4,
   830  		MemoryGB:        56,
   831  		GPU:             1,
   832  		BootAfterCreate: false,
   833  		Client:          NewBuildersAPIClient(testutil.SingletonAPICaller()),
   834  		ForceShutdown:   true,
   835  	}
   836  	withGPU, err := builder.Build(ctx, zone)
   837  	if err != nil {
   838  		t.Fatal(err)
   839  	}
   840  
   841  	// for update
   842  	builder.ServerID = withGPU.ServerID
   843  	builder.GPU = 0
   844  	_, err = builder.Update(ctx, zone)
   845  	require.Error(t, err, "server plan not found")
   846  
   847  	// cleanup
   848  	serverOp := sacloud.NewServerOp(testutil.SingletonAPICaller())
   849  	if err := serverOp.Delete(ctx, zone, withGPU.ServerID); err != nil {
   850  		t.Fatal(err)
   851  	}
   852  }