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 }