github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/vsphere/internal/vsphereclient/createvm_test.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package vsphereclient
     5  
     6  import (
     7  	"bytes"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"time"
    12  
    13  	"github.com/juju/clock/testclock"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/vmware/govmomi/vim25/mo"
    17  	"github.com/vmware/govmomi/vim25/types"
    18  	"golang.org/x/net/context"
    19  	gc "gopkg.in/check.v1"
    20  
    21  	"github.com/juju/juju/core/constraints"
    22  	"github.com/juju/juju/provider/vsphere/internal/ovatest"
    23  	coretesting "github.com/juju/juju/testing"
    24  )
    25  
    26  func (s *clientSuite) TestCreateVirtualMachine(c *gc.C) {
    27  	var statusUpdates []string
    28  	statusUpdatesCh := make(chan string, 4)
    29  	dequeueStatusUpdates := func() {
    30  		for {
    31  			select {
    32  			case <-statusUpdatesCh:
    33  			default:
    34  				return
    35  			}
    36  		}
    37  	}
    38  
    39  	args := baseCreateVirtualMachineParams(c)
    40  	testClock := args.Clock.(*testclock.Clock)
    41  	s.onImageUpload = func(r *http.Request) {
    42  		dequeueStatusUpdates()
    43  
    44  		// Wait 1.5 seconds, which is long enough to trigger the
    45  		// status update timer.
    46  		testClock.WaitAdvance(1500*time.Millisecond, coretesting.LongWait, 1)
    47  
    48  		// Waiting for the status update here guarantees that a report is
    49  		// available, since we don't update status until that is true.
    50  		<-statusUpdatesCh
    51  
    52  		s.onImageUpload = nil
    53  	}
    54  	args.UpdateProgress = func(status string) {
    55  		statusUpdatesCh <- status
    56  		statusUpdates = append(statusUpdates, status)
    57  	}
    58  
    59  	client := s.newFakeClient(&s.roundTripper, "dc0")
    60  	_, err := client.CreateVirtualMachine(context.Background(), args)
    61  	c.Assert(err, jc.ErrorIsNil)
    62  	c.Assert(statusUpdates, jc.DeepEquals, []string{
    63  		"uploading juju-vmdks/ctrl/xenial/4d9f679a703b95c99189eab283c8c1b36caa062321c531f3dac8163a59c70087.vmdk.tmp: 100.00% (0B/s)",
    64  		"creating import spec",
    65  		`creating VM "vm-0"`,
    66  		"VM cloned",
    67  		"powering on",
    68  	})
    69  
    70  	c.Assert(s.uploadRequests, gc.HasLen, 1)
    71  	contents, err := ioutil.ReadAll(s.uploadRequests[0].Body)
    72  	c.Assert(err, jc.ErrorIsNil)
    73  	c.Assert(string(contents), gc.Equals, "FakeVmdkContent")
    74  
    75  	s.roundTripper.CheckCalls(c, []testing.StubCall{
    76  		retrievePropertiesStubCall("FakeRootFolder"),
    77  		retrievePropertiesStubCall("FakeRootFolder"),
    78  		retrievePropertiesStubCall("FakeDatacenter"),
    79  		retrievePropertiesStubCall("FakeRootFolder"),
    80  		retrievePropertiesStubCall("FakeDatacenter"),
    81  		retrievePropertiesStubCall("FakeVmFolder"),
    82  		retrievePropertiesStubCall("FakeHostFolder"),
    83  		retrievePropertiesStubCall("FakeDatastore1", "FakeDatastore2"),
    84  		retrievePropertiesStubCall("FakeDatastore2"),
    85  
    86  		{"SearchDatastore", []interface{}{
    87  			"[datastore2] juju-vmdks/ctrl/xenial",
    88  			&types.HostDatastoreBrowserSearchSpec{
    89  				MatchPattern: []string{"4d9f679a703b95c99189eab283c8c1b36caa062321c531f3dac8163a59c70087.vmdk"},
    90  				Details: &types.FileQueryFlags{
    91  					FileType:     true,
    92  					FileSize:     true,
    93  					Modification: true,
    94  					FileOwner:    newBool(true),
    95  				},
    96  			},
    97  		}},
    98  		{"CreatePropertyCollector", nil},
    99  		{"CreateFilter", nil},
   100  		{"WaitForUpdatesEx", nil},
   101  
   102  		{"DeleteDatastoreFile", []interface{}{
   103  			"[datastore2] juju-vmdks/ctrl/xenial",
   104  		}},
   105  		{"CreatePropertyCollector", nil},
   106  		{"CreateFilter", nil},
   107  		{"WaitForUpdatesEx", nil},
   108  
   109  		{"MakeDirectory", []interface{}{
   110  			"[datastore2] juju-vmdks/ctrl/xenial",
   111  		}},
   112  
   113  		{"MoveDatastoreFile", []interface{}{
   114  			"[datastore2] juju-vmdks/ctrl/xenial/4d9f679a703b95c99189eab283c8c1b36caa062321c531f3dac8163a59c70087.vmdk.tmp",
   115  			"[datastore2] juju-vmdks/ctrl/xenial/4d9f679a703b95c99189eab283c8c1b36caa062321c531f3dac8163a59c70087.vmdk",
   116  			newBool(true),
   117  		}},
   118  		{"CreatePropertyCollector", nil},
   119  		{"CreateFilter", nil},
   120  		{"WaitForUpdatesEx", nil},
   121  
   122  		{"CreateImportSpec", []interface{}{
   123  			UbuntuOVF,
   124  			types.ManagedObjectReference{Type: "Datastore", Value: "FakeDatastore2"},
   125  			baseCisp(),
   126  		}},
   127  		retrievePropertiesStubCall("network-0", "network-1"),
   128  		retrievePropertiesStubCall("onetwork-0"),
   129  		retrievePropertiesStubCall("dvportgroup-0"),
   130  		{"ImportVApp", []interface{}{&types.VirtualMachineImportSpec{
   131  			ConfigSpec: types.VirtualMachineConfigSpec{
   132  				Name: "vm-name.tmp",
   133  				ExtraConfig: []types.BaseOptionValue{
   134  					&types.OptionValue{Key: "k", Value: "v"},
   135  				},
   136  				Flags: &types.VirtualMachineFlagInfo{DiskUuidEnabled: newBool(true)},
   137  			},
   138  		}}},
   139  		{"CreatePropertyCollector", nil},
   140  		{"CreateFilter", nil},
   141  		{"WaitForUpdatesEx", nil},
   142  
   143  		{"HttpNfcLeaseComplete", []interface{}{"FakeLease"}},
   144  
   145  		{"CloneVM_Task", nil},
   146  		{"CreatePropertyCollector", nil},
   147  		{"CreateFilter", nil},
   148  		{"WaitForUpdatesEx", nil},
   149  
   150  		retrievePropertiesStubCall("FakeVm0"),
   151  
   152  		{"ReconfigVM_Task", nil},
   153  		{"CreatePropertyCollector", nil},
   154  		{"CreateFilter", nil},
   155  		{"WaitForUpdatesEx", nil},
   156  
   157  		{"PowerOnVM_Task", nil},
   158  		{"CreatePropertyCollector", nil},
   159  		{"CreateFilter", nil},
   160  		{"WaitForUpdatesEx", nil},
   161  
   162  		retrievePropertiesStubCall(""),
   163  
   164  		{"Destroy_Task", nil},
   165  		{"CreatePropertyCollector", nil},
   166  		{"CreateFilter", nil},
   167  		{"WaitForUpdatesEx", nil},
   168  	})
   169  }
   170  
   171  func (s *clientSuite) TestCreateVirtualMachineNoDiskUUID(c *gc.C) {
   172  	args := baseCreateVirtualMachineParams(c)
   173  	args.EnableDiskUUID = false
   174  	client := s.newFakeClient(&s.roundTripper, "dc0")
   175  	_, err := client.CreateVirtualMachine(context.Background(), args)
   176  	c.Assert(err, jc.ErrorIsNil)
   177  
   178  	s.roundTripper.CheckCall(c, 26, "ImportVApp", &types.VirtualMachineImportSpec{
   179  		ConfigSpec: types.VirtualMachineConfigSpec{
   180  			Name: "vm-name.tmp",
   181  			ExtraConfig: []types.BaseOptionValue{
   182  				&types.OptionValue{Key: "k", Value: "v"},
   183  			},
   184  			Flags: &types.VirtualMachineFlagInfo{DiskUuidEnabled: newBool(false)},
   185  		},
   186  	})
   187  }
   188  
   189  func (s *clientSuite) TestCreateVirtualMachineVMDKDirectoryNotFound(c *gc.C) {
   190  	// FileNotFound is returned when the *directory* doesn't exist.
   191  	s.roundTripper.taskError[searchDatastoreTask] = &types.LocalizedMethodFault{
   192  		Fault: &types.FileNotFound{},
   193  	}
   194  
   195  	args := baseCreateVirtualMachineParams(c)
   196  	client := s.newFakeClient(&s.roundTripper, "dc0")
   197  	_, err := client.CreateVirtualMachine(context.Background(), args)
   198  	c.Assert(err, jc.ErrorIsNil)
   199  
   200  	calls := s.roundTripper.Calls()
   201  	assertNoCall(c, calls, "DeleteDatastoreFile")
   202  	findStubCall(c, calls, "MakeDirectory")
   203  }
   204  
   205  func (s *clientSuite) TestCreateVirtualMachineDiskAlreadyCached(c *gc.C) {
   206  	results := types.HostDatastoreBrowserSearchResults{
   207  		File: []types.BaseFileInfo{&types.VmDiskFileInfo{}},
   208  	}
   209  	s.roundTripper.taskResult[searchDatastoreTask] = results
   210  
   211  	args := baseCreateVirtualMachineParams(c)
   212  	client := s.newFakeClient(&s.roundTripper, "dc0")
   213  	_, err := client.CreateVirtualMachine(context.Background(), args)
   214  	c.Assert(err, jc.ErrorIsNil)
   215  
   216  	// There should be no upload, and the VMDK directory should neither
   217  	// have been deleted nor created.
   218  	calls := s.roundTripper.Calls()
   219  	assertNoCall(c, calls, "DeleteDatastoreFile")
   220  	assertNoCall(c, calls, "MakeDirectory")
   221  	c.Assert(s.uploadRequests, gc.HasLen, 0)
   222  }
   223  
   224  func (s *clientSuite) TestCreateVirtualMachineDatastoreSpecified(c *gc.C) {
   225  	args := baseCreateVirtualMachineParams(c)
   226  	args.Datastore = "datastore1"
   227  	args.ComputeResource.Datastore = []types.ManagedObjectReference{{
   228  		Type:  "Datastore",
   229  		Value: "FakeDatastore2",
   230  	}, {
   231  		Type:  "Datastore",
   232  		Value: "FakeDatastore1",
   233  	}}
   234  
   235  	client := s.newFakeClient(&s.roundTripper, "dc0")
   236  	_, err := client.CreateVirtualMachine(context.Background(), args)
   237  	c.Assert(err, jc.ErrorIsNil)
   238  
   239  	s.roundTripper.CheckCall(
   240  		c, 22, "CreateImportSpec", UbuntuOVF,
   241  		types.ManagedObjectReference{Type: "Datastore", Value: "FakeDatastore1"},
   242  		baseCisp(),
   243  	)
   244  }
   245  
   246  func (s *clientSuite) TestCreateVirtualMachineDatastoreNotFound(c *gc.C) {
   247  	args := baseCreateVirtualMachineParams(c)
   248  	args.Datastore = "datastore3"
   249  
   250  	client := s.newFakeClient(&s.roundTripper, "dc0")
   251  	_, err := client.CreateVirtualMachine(context.Background(), args)
   252  	c.Assert(err, gc.ErrorMatches, `could not find datastore "datastore3"`)
   253  }
   254  
   255  func (s *clientSuite) TestCreateVirtualMachineDatastoreNoneAccessible(c *gc.C) {
   256  	args := baseCreateVirtualMachineParams(c)
   257  	args.ComputeResource.Datastore = []types.ManagedObjectReference{{
   258  		Type:  "Datastore",
   259  		Value: "FakeDatastore1",
   260  	}}
   261  
   262  	client := s.newFakeClient(&s.roundTripper, "dc0")
   263  	_, err := client.CreateVirtualMachine(context.Background(), args)
   264  	c.Assert(err, gc.ErrorMatches, "could not find an accessible datastore")
   265  }
   266  
   267  func (s *clientSuite) TestCreateVirtualMachineMultipleNetworksSpecifiedFirstDefault(c *gc.C) {
   268  	args := baseCreateVirtualMachineParams(c)
   269  	args.NetworkDevices = []NetworkDevice{
   270  		{MAC: "00:50:56:11:22:33"},
   271  		{Network: "arpa"},
   272  	}
   273  
   274  	client := s.newFakeClient(&s.roundTripper, "dc0")
   275  	_, err := client.CreateVirtualMachine(context.Background(), args)
   276  	c.Assert(err, jc.ErrorIsNil)
   277  
   278  	var networkDevice1, networkDevice2 types.VirtualVmxnet3
   279  	wakeOnLan := true
   280  	networkDevice1.WakeOnLanEnabled = &wakeOnLan
   281  	networkDevice1.Connectable = &types.VirtualDeviceConnectInfo{
   282  		StartConnected:    true,
   283  		AllowGuestControl: true,
   284  	}
   285  	networkDevice1.AddressType = "Manual"
   286  	networkDevice1.MacAddress = "00:50:56:11:22:33"
   287  	networkDevice1.Backing = &types.VirtualEthernetCardNetworkBackingInfo{
   288  		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
   289  			DeviceName: "VM Network",
   290  		},
   291  	}
   292  
   293  	networkDevice2.WakeOnLanEnabled = &wakeOnLan
   294  	networkDevice2.Connectable = &types.VirtualDeviceConnectInfo{
   295  		StartConnected:    true,
   296  		AllowGuestControl: true,
   297  	}
   298  	networkDevice2.Backing = &types.VirtualEthernetCardNetworkBackingInfo{
   299  		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
   300  			DeviceName: "arpa",
   301  		},
   302  	}
   303  
   304  	s.roundTripper.CheckCall(c, 26, "ImportVApp", &types.VirtualMachineImportSpec{
   305  		ConfigSpec: types.VirtualMachineConfigSpec{
   306  			Name: "vm-name.tmp",
   307  			ExtraConfig: []types.BaseOptionValue{
   308  				&types.OptionValue{Key: "k", Value: "v"},
   309  			},
   310  			DeviceChange: []types.BaseVirtualDeviceConfigSpec{
   311  				&types.VirtualDeviceConfigSpec{
   312  					Operation: "add",
   313  					Device:    &networkDevice1,
   314  				},
   315  				&types.VirtualDeviceConfigSpec{
   316  					Operation: "add",
   317  					Device:    &networkDevice2,
   318  				},
   319  			},
   320  			Flags: &types.VirtualMachineFlagInfo{DiskUuidEnabled: newBool(true)},
   321  		},
   322  	})
   323  }
   324  
   325  func (s *clientSuite) TestCreateVirtualMachineNetworkSpecifiedDVPortgroup(c *gc.C) {
   326  	args := baseCreateVirtualMachineParams(c)
   327  	args.NetworkDevices = []NetworkDevice{
   328  		{Network: "yoink"},
   329  	}
   330  
   331  	client := s.newFakeClient(&s.roundTripper, "dc0")
   332  	_, err := client.CreateVirtualMachine(context.Background(), args)
   333  	c.Assert(err, jc.ErrorIsNil)
   334  
   335  	var networkDevice types.VirtualVmxnet3
   336  	wakeOnLan := true
   337  	networkDevice.WakeOnLanEnabled = &wakeOnLan
   338  	networkDevice.Connectable = &types.VirtualDeviceConnectInfo{
   339  		StartConnected:    true,
   340  		AllowGuestControl: true,
   341  	}
   342  	networkDevice.Backing = &types.VirtualEthernetCardDistributedVirtualPortBackingInfo{
   343  		Port: types.DistributedVirtualSwitchPortConnection{
   344  			SwitchUuid:   "yup",
   345  			PortgroupKey: "hole",
   346  		},
   347  	}
   348  
   349  	retrieveDVSCall := retrievePropertiesStubCall("dvs-0")
   350  	s.roundTripper.CheckCall(c, 26, retrieveDVSCall.FuncName, retrieveDVSCall.Args...)
   351  
   352  	// When the external network is a distributed virtual portgroup,
   353  	// we must make an additional RetrieveProperties call to fetch
   354  	// the DVS's UUID. This bumps the ImportVApp position by one.
   355  	s.roundTripper.CheckCall(c, 27, "ImportVApp", &types.VirtualMachineImportSpec{
   356  		ConfigSpec: types.VirtualMachineConfigSpec{
   357  			Name: "vm-name.tmp",
   358  			ExtraConfig: []types.BaseOptionValue{
   359  				&types.OptionValue{Key: "k", Value: "v"},
   360  			},
   361  			DeviceChange: []types.BaseVirtualDeviceConfigSpec{
   362  				&types.VirtualDeviceConfigSpec{
   363  					Operation: "add",
   364  					Device:    &networkDevice,
   365  				},
   366  			},
   367  			Flags: &types.VirtualMachineFlagInfo{DiskUuidEnabled: newBool(true)},
   368  		},
   369  	})
   370  }
   371  
   372  func (s *clientSuite) TestCreateVirtualMachineNetworkNotFound(c *gc.C) {
   373  	args := baseCreateVirtualMachineParams(c)
   374  	args.NetworkDevices = []NetworkDevice{
   375  		{Network: "fourtytwo"},
   376  	}
   377  
   378  	client := s.newFakeClient(&s.roundTripper, "dc0")
   379  	_, err := client.CreateVirtualMachine(context.Background(), args)
   380  	c.Assert(err, gc.ErrorMatches, `creating import spec: network "fourtytwo" not found`)
   381  }
   382  
   383  func (s *clientSuite) TestCreateVirtualMachineInvalidMAC(c *gc.C) {
   384  	args := baseCreateVirtualMachineParams(c)
   385  	args.NetworkDevices = []NetworkDevice{
   386  		{MAC: "00:11:22:33:44:55"},
   387  	}
   388  
   389  	client := s.newFakeClient(&s.roundTripper, "dc0")
   390  	_, err := client.CreateVirtualMachine(context.Background(), args)
   391  	c.Assert(err, gc.ErrorMatches, `creating import spec: adding network device 0 - network VM Network: Invalid MAC address: "00:11:22:33:44:55"`)
   392  }
   393  
   394  func (s *clientSuite) TestCreateVirtualMachineRootDiskSize(c *gc.C) {
   395  	args := baseCreateVirtualMachineParams(c)
   396  	rootDisk := uint64(1024 * 20) // 20 GiB
   397  	args.Constraints.RootDisk = &rootDisk
   398  
   399  	client := s.newFakeClient(&s.roundTripper, "dc0")
   400  	_, err := client.CreateVirtualMachine(context.Background(), args)
   401  	c.Assert(err, jc.ErrorIsNil)
   402  
   403  	call := findStubCall(c, s.roundTripper.Calls(), "ExtendVirtualDisk")
   404  	c.Assert(call.Args, jc.DeepEquals, []interface{}{
   405  		"disk.vmdk",
   406  		int64(rootDisk) * 1024, // in KiB
   407  	})
   408  }
   409  
   410  func (s *clientSuite) TestVerifyMAC(c *gc.C) {
   411  	var testData = []struct {
   412  		Mac    string
   413  		Result bool
   414  	}{
   415  		{"foo:bar:baz", false},
   416  		{"00:22:55:11:34:11", false},
   417  		{"00:50:56:123:11:11", false},
   418  		{"00:50:56:40:12:23", false},
   419  		{"00:50:56:3f:ff:ff", true},
   420  		{"00:50:56:12:34:56", true},
   421  		{"00:50:56:2A:eB:Cd", true},
   422  		{"00:50:56:2a:xy:cd", false},
   423  		{"00:50:560:2a:xy:cd", false},
   424  	}
   425  	for i, test := range testData {
   426  		c.Logf("test #%d: MAC=%s expected %s", i, test.Mac, test.Result)
   427  		c.Check(VerifyMAC(test.Mac), gc.Equals, test.Result)
   428  	}
   429  }
   430  
   431  func baseCreateVirtualMachineParams(c *gc.C) CreateVirtualMachineParams {
   432  	readOVA := func() (string, io.ReadCloser, error) {
   433  		r := bytes.NewReader(ovatest.FakeOVAContents())
   434  		return "fake-ova-location", ioutil.NopCloser(r), nil
   435  	}
   436  
   437  	return CreateVirtualMachineParams{
   438  		Name:          "vm-0",
   439  		Folder:        "foo",
   440  		ReadOVA:       readOVA,
   441  		OVASHA256:     ovatest.FakeOVASHA256(),
   442  		VMDKDirectory: "juju-vmdks/ctrl",
   443  		Series:        "xenial",
   444  		UserData:      "baz",
   445  		ComputeResource: &mo.ComputeResource{
   446  			ResourcePool: &types.ManagedObjectReference{
   447  				Type:  "ResourcePool",
   448  				Value: "FakeResourcePool1",
   449  			},
   450  			Datastore: []types.ManagedObjectReference{{
   451  				Type:  "Datastore",
   452  				Value: "FakeDatastore1",
   453  			}, {
   454  				Type:  "Datastore",
   455  				Value: "FakeDatastore2",
   456  			}},
   457  			Network: []types.ManagedObjectReference{{
   458  				Type:  "Network",
   459  				Value: "network-0",
   460  			}, {
   461  				Type:  "Network",
   462  				Value: "network-1",
   463  			}, {
   464  				Type:  "OpaqueNetwork",
   465  				Value: "onetwork-0",
   466  			}, {
   467  				Type:  "DistributedVirtualPortgroup",
   468  				Value: "dvportgroup-0",
   469  			}},
   470  		},
   471  		Metadata:               map[string]string{"k": "v"},
   472  		Constraints:            constraints.Value{},
   473  		UpdateProgress:         func(status string) {},
   474  		UpdateProgressInterval: time.Second,
   475  		Clock:                  testclock.NewClock(time.Time{}),
   476  		EnableDiskUUID:         true,
   477  	}
   478  }
   479  
   480  func baseCisp() types.OvfCreateImportSpecParams {
   481  	return types.OvfCreateImportSpecParams{
   482  		EntityName: "vm-0",
   483  		PropertyMapping: []types.KeyValue{
   484  			{Key: "user-data", Value: "baz"},
   485  			{Key: "hostname", Value: "vm-0"},
   486  		},
   487  	}
   488  }
   489  
   490  func newBool(v bool) *bool {
   491  	return &v
   492  }
   493  
   494  func findStubCall(c *gc.C, calls []testing.StubCall, name string) testing.StubCall {
   495  	for _, call := range calls {
   496  		if call.FuncName == name {
   497  			return call
   498  		}
   499  	}
   500  	c.Fatalf("failed to find call %q", name)
   501  	panic("unreachable")
   502  }
   503  
   504  func assertNoCall(c *gc.C, calls []testing.StubCall, name string) {
   505  	for _, call := range calls {
   506  		if call.FuncName == name {
   507  			c.Fatalf("found call %q", name)
   508  		}
   509  	}
   510  }