1/*
2Copyright (c) 2014-2016 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package vm
18
19import (
20	"context"
21	"flag"
22	"fmt"
23	"strings"
24
25	"github.com/vmware/govmomi/govc/cli"
26	"github.com/vmware/govmomi/govc/flags"
27	"github.com/vmware/govmomi/object"
28	"github.com/vmware/govmomi/property"
29	"github.com/vmware/govmomi/units"
30	"github.com/vmware/govmomi/vim25"
31	"github.com/vmware/govmomi/vim25/mo"
32	"github.com/vmware/govmomi/vim25/types"
33)
34
35type create struct {
36	*flags.ClientFlag
37	*flags.DatacenterFlag
38	*flags.DatastoreFlag
39	*flags.StoragePodFlag
40	*flags.ResourcePoolFlag
41	*flags.HostSystemFlag
42	*flags.NetworkFlag
43	*flags.FolderFlag
44
45	name       string
46	memory     int
47	cpus       int
48	guestID    string
49	link       bool
50	on         bool
51	force      bool
52	controller string
53	annotation string
54	firmware   string
55
56	iso              string
57	isoDatastoreFlag *flags.DatastoreFlag
58	isoDatastore     *object.Datastore
59
60	disk              string
61	diskDatastoreFlag *flags.DatastoreFlag
62	diskDatastore     *object.Datastore
63
64	// Only set if the disk argument is a byte size, which means the disk
65	// doesn't exist yet and should be created
66	diskByteSize int64
67
68	Client       *vim25.Client
69	Datacenter   *object.Datacenter
70	Datastore    *object.Datastore
71	StoragePod   *object.StoragePod
72	ResourcePool *object.ResourcePool
73	HostSystem   *object.HostSystem
74	Folder       *object.Folder
75}
76
77func init() {
78	cli.Register("vm.create", &create{})
79}
80
81func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
82	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
83	cmd.ClientFlag.Register(ctx, f)
84
85	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
86	cmd.DatacenterFlag.Register(ctx, f)
87
88	cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx)
89	cmd.DatastoreFlag.Register(ctx, f)
90
91	cmd.StoragePodFlag, ctx = flags.NewStoragePodFlag(ctx)
92	cmd.StoragePodFlag.Register(ctx, f)
93
94	cmd.ResourcePoolFlag, ctx = flags.NewResourcePoolFlag(ctx)
95	cmd.ResourcePoolFlag.Register(ctx, f)
96
97	cmd.HostSystemFlag, ctx = flags.NewHostSystemFlag(ctx)
98	cmd.HostSystemFlag.Register(ctx, f)
99
100	cmd.NetworkFlag, ctx = flags.NewNetworkFlag(ctx)
101	cmd.NetworkFlag.Register(ctx, f)
102
103	cmd.FolderFlag, ctx = flags.NewFolderFlag(ctx)
104	cmd.FolderFlag.Register(ctx, f)
105
106	f.IntVar(&cmd.memory, "m", 1024, "Size in MB of memory")
107	f.IntVar(&cmd.cpus, "c", 1, "Number of CPUs")
108	f.StringVar(&cmd.guestID, "g", "otherGuest", "Guest OS ID")
109	f.BoolVar(&cmd.link, "link", true, "Link specified disk")
110	f.BoolVar(&cmd.on, "on", true, "Power on VM. Default is true if -disk argument is given.")
111	f.BoolVar(&cmd.force, "force", false, "Create VM if vmx already exists")
112	f.StringVar(&cmd.controller, "disk.controller", "scsi", "Disk controller type")
113	f.StringVar(&cmd.annotation, "annotation", "", "VM description")
114
115	firmwareTypes := []string{
116		string(types.GuestOsDescriptorFirmwareTypeBios),
117		string(types.GuestOsDescriptorFirmwareTypeEfi),
118	}
119
120	f.StringVar(&cmd.firmware, "firmware", firmwareTypes[0],
121		fmt.Sprintf("Firmware type [%s]", strings.Join(firmwareTypes, "|")))
122
123	f.StringVar(&cmd.iso, "iso", "", "ISO path")
124	cmd.isoDatastoreFlag, ctx = flags.NewCustomDatastoreFlag(ctx)
125	f.StringVar(&cmd.isoDatastoreFlag.Name, "iso-datastore", "", "Datastore for ISO file")
126
127	f.StringVar(&cmd.disk, "disk", "", "Disk path (to use existing) OR size (to create new, e.g. 20GB)")
128	cmd.diskDatastoreFlag, _ = flags.NewCustomDatastoreFlag(ctx)
129	f.StringVar(&cmd.diskDatastoreFlag.Name, "disk-datastore", "", "Datastore for disk file")
130}
131
132func (cmd *create) Process(ctx context.Context) error {
133	if err := cmd.ClientFlag.Process(ctx); err != nil {
134		return err
135	}
136	if err := cmd.DatacenterFlag.Process(ctx); err != nil {
137		return err
138	}
139	if err := cmd.DatastoreFlag.Process(ctx); err != nil {
140		return err
141	}
142	if err := cmd.StoragePodFlag.Process(ctx); err != nil {
143		return err
144	}
145	if err := cmd.ResourcePoolFlag.Process(ctx); err != nil {
146		return err
147	}
148	if err := cmd.HostSystemFlag.Process(ctx); err != nil {
149		return err
150	}
151	if err := cmd.NetworkFlag.Process(ctx); err != nil {
152		return err
153	}
154	if err := cmd.FolderFlag.Process(ctx); err != nil {
155		return err
156	}
157
158	// Default iso/disk datastores to the VM's datastore
159	if cmd.isoDatastoreFlag.Name == "" {
160		cmd.isoDatastoreFlag = cmd.DatastoreFlag
161	}
162	if cmd.diskDatastoreFlag.Name == "" {
163		cmd.diskDatastoreFlag = cmd.DatastoreFlag
164	}
165
166	return nil
167}
168
169func (cmd *create) Usage() string {
170	return "NAME"
171}
172
173func (cmd *create) Description() string {
174	return `Create VM.
175
176For a list of possible '-g' IDs, see:
177http://pubs.vmware.com/vsphere-6-5/topic/com.vmware.wssdk.apiref.doc/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html
178
179Examples:
180  govc vm.create vm-name
181  govc vm.create -m 2048 -c 2 -g freebsd64Guest -net.adapter vmxnet3 -disk.controller pvscsi vm-name`
182}
183
184func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
185	var err error
186
187	if len(f.Args()) != 1 {
188		return flag.ErrHelp
189	}
190
191	cmd.name = f.Arg(0)
192	if cmd.name == "" {
193		return flag.ErrHelp
194	}
195
196	cmd.Client, err = cmd.ClientFlag.Client()
197	if err != nil {
198		return err
199	}
200
201	cmd.Datacenter, err = cmd.DatacenterFlag.Datacenter()
202	if err != nil {
203		return err
204	}
205
206	if cmd.StoragePodFlag.Isset() {
207		cmd.StoragePod, err = cmd.StoragePodFlag.StoragePod()
208		if err != nil {
209			return err
210		}
211	} else {
212		cmd.Datastore, err = cmd.DatastoreFlag.Datastore()
213		if err != nil {
214			return err
215		}
216	}
217
218	cmd.HostSystem, err = cmd.HostSystemFlag.HostSystemIfSpecified()
219	if err != nil {
220		return err
221	}
222
223	if cmd.HostSystem != nil {
224		if cmd.ResourcePool, err = cmd.HostSystem.ResourcePool(ctx); err != nil {
225			return err
226		}
227	} else {
228		// -host is optional
229		if cmd.ResourcePool, err = cmd.ResourcePoolFlag.ResourcePool(); err != nil {
230			return err
231		}
232	}
233
234	if cmd.Folder, err = cmd.FolderFlag.Folder(); err != nil {
235		return err
236	}
237
238	// Verify ISO exists
239	if cmd.iso != "" {
240		_, err = cmd.isoDatastoreFlag.Stat(ctx, cmd.iso)
241		if err != nil {
242			return err
243		}
244
245		cmd.isoDatastore, err = cmd.isoDatastoreFlag.Datastore()
246		if err != nil {
247			return err
248		}
249	}
250
251	// Verify disk exists
252	if cmd.disk != "" {
253		var b units.ByteSize
254
255		// If disk can be parsed as byte units, don't stat
256		err = b.Set(cmd.disk)
257		if err == nil {
258			cmd.diskByteSize = int64(b)
259		} else {
260			_, err = cmd.diskDatastoreFlag.Stat(ctx, cmd.disk)
261			if err != nil {
262				return err
263			}
264
265			cmd.diskDatastore, err = cmd.diskDatastoreFlag.Datastore()
266			if err != nil {
267				return err
268			}
269		}
270	}
271
272	task, err := cmd.createVM(ctx)
273	if err != nil {
274		return err
275	}
276
277	info, err := task.WaitForResult(ctx, nil)
278	if err != nil {
279		return err
280	}
281
282	vm := object.NewVirtualMachine(cmd.Client, info.Result.(types.ManagedObjectReference))
283
284	if cmd.on {
285		task, err := vm.PowerOn(ctx)
286		if err != nil {
287			return err
288		}
289
290		_, err = task.WaitForResult(ctx, nil)
291		if err != nil {
292			return err
293		}
294	}
295
296	return nil
297}
298
299func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
300	var devices object.VirtualDeviceList
301	var err error
302
303	spec := &types.VirtualMachineConfigSpec{
304		Name:       cmd.name,
305		GuestId:    cmd.guestID,
306		NumCPUs:    int32(cmd.cpus),
307		MemoryMB:   int64(cmd.memory),
308		Annotation: cmd.annotation,
309		Firmware:   cmd.firmware,
310	}
311
312	devices, err = cmd.addStorage(nil)
313	if err != nil {
314		return nil, err
315	}
316
317	devices, err = cmd.addNetwork(devices)
318	if err != nil {
319		return nil, err
320	}
321
322	deviceChange, err := devices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
323	if err != nil {
324		return nil, err
325	}
326
327	spec.DeviceChange = deviceChange
328
329	var datastore *object.Datastore
330
331	// If storage pod is specified, collect placement recommendations
332	if cmd.StoragePod != nil {
333		datastore, err = cmd.recommendDatastore(ctx, spec)
334		if err != nil {
335			return nil, err
336		}
337	} else {
338		datastore = cmd.Datastore
339	}
340
341	if !cmd.force {
342		vmxPath := fmt.Sprintf("%s/%s.vmx", cmd.name, cmd.name)
343
344		_, err := datastore.Stat(ctx, vmxPath)
345		if err == nil {
346			dsPath := cmd.Datastore.Path(vmxPath)
347			return nil, fmt.Errorf("File %s already exists", dsPath)
348		}
349	}
350
351	folder := cmd.Folder
352
353	spec.Files = &types.VirtualMachineFileInfo{
354		VmPathName: fmt.Sprintf("[%s]", datastore.Name()),
355	}
356
357	return folder.CreateVM(ctx, *spec, cmd.ResourcePool, cmd.HostSystem)
358}
359
360func (cmd *create) addStorage(devices object.VirtualDeviceList) (object.VirtualDeviceList, error) {
361	if cmd.controller != "ide" {
362		if cmd.controller == "nvme" {
363			nvme, err := devices.CreateNVMEController()
364			if err != nil {
365				return nil, err
366			}
367
368			devices = append(devices, nvme)
369			cmd.controller = devices.Name(nvme)
370		} else {
371			scsi, err := devices.CreateSCSIController(cmd.controller)
372			if err != nil {
373				return nil, err
374			}
375
376			devices = append(devices, scsi)
377			cmd.controller = devices.Name(scsi)
378		}
379	}
380
381	// If controller is specified to be IDE or if an ISO is specified, add IDE controller.
382	if cmd.controller == "ide" || cmd.iso != "" {
383		ide, err := devices.CreateIDEController()
384		if err != nil {
385			return nil, err
386		}
387
388		devices = append(devices, ide)
389	}
390
391	if cmd.diskByteSize != 0 {
392		controller, err := devices.FindDiskController(cmd.controller)
393		if err != nil {
394			return nil, err
395		}
396
397		disk := &types.VirtualDisk{
398			VirtualDevice: types.VirtualDevice{
399				Key: devices.NewKey(),
400				Backing: &types.VirtualDiskFlatVer2BackingInfo{
401					DiskMode:        string(types.VirtualDiskModePersistent),
402					ThinProvisioned: types.NewBool(true),
403				},
404			},
405			CapacityInKB: cmd.diskByteSize / 1024,
406		}
407
408		devices.AssignController(disk, controller)
409		devices = append(devices, disk)
410	} else if cmd.disk != "" {
411		controller, err := devices.FindDiskController(cmd.controller)
412		if err != nil {
413			return nil, err
414		}
415
416		ds := cmd.diskDatastore.Reference()
417		path := cmd.diskDatastore.Path(cmd.disk)
418		disk := devices.CreateDisk(controller, ds, path)
419
420		if cmd.link {
421			disk = devices.ChildDisk(disk)
422		}
423
424		devices = append(devices, disk)
425	}
426
427	if cmd.iso != "" {
428		ide, err := devices.FindIDEController("")
429		if err != nil {
430			return nil, err
431		}
432
433		cdrom, err := devices.CreateCdrom(ide)
434		if err != nil {
435			return nil, err
436		}
437
438		cdrom = devices.InsertIso(cdrom, cmd.isoDatastore.Path(cmd.iso))
439		devices = append(devices, cdrom)
440	}
441
442	return devices, nil
443}
444
445func (cmd *create) addNetwork(devices object.VirtualDeviceList) (object.VirtualDeviceList, error) {
446	netdev, err := cmd.NetworkFlag.Device()
447	if err != nil {
448		return nil, err
449	}
450
451	devices = append(devices, netdev)
452	return devices, nil
453}
454
455func (cmd *create) recommendDatastore(ctx context.Context, spec *types.VirtualMachineConfigSpec) (*object.Datastore, error) {
456	sp := cmd.StoragePod.Reference()
457
458	// Build pod selection spec from config spec
459	podSelectionSpec := types.StorageDrsPodSelectionSpec{
460		StoragePod: &sp,
461	}
462
463	// Keep list of disks that need to be placed
464	var disks []*types.VirtualDisk
465
466	// Collect disks eligible for placement
467	for _, deviceConfigSpec := range spec.DeviceChange {
468		s := deviceConfigSpec.GetVirtualDeviceConfigSpec()
469		if s.Operation != types.VirtualDeviceConfigSpecOperationAdd {
470			continue
471		}
472
473		if s.FileOperation != types.VirtualDeviceConfigSpecFileOperationCreate {
474			continue
475		}
476
477		d, ok := s.Device.(*types.VirtualDisk)
478		if !ok {
479			continue
480		}
481
482		podConfigForPlacement := types.VmPodConfigForPlacement{
483			StoragePod: sp,
484			Disk: []types.PodDiskLocator{
485				{
486					DiskId:          d.Key,
487					DiskBackingInfo: d.Backing,
488				},
489			},
490		}
491
492		podSelectionSpec.InitialVmConfig = append(podSelectionSpec.InitialVmConfig, podConfigForPlacement)
493		disks = append(disks, d)
494	}
495
496	sps := types.StoragePlacementSpec{
497		Type:             string(types.StoragePlacementSpecPlacementTypeCreate),
498		ResourcePool:     types.NewReference(cmd.ResourcePool.Reference()),
499		PodSelectionSpec: podSelectionSpec,
500		ConfigSpec:       spec,
501	}
502
503	srm := object.NewStorageResourceManager(cmd.Client)
504	result, err := srm.RecommendDatastores(ctx, sps)
505	if err != nil {
506		return nil, err
507	}
508
509	// Use result to pin disks to recommended datastores
510	recs := result.Recommendations
511	if len(recs) == 0 {
512		return nil, fmt.Errorf("no recommendations")
513	}
514
515	ds := recs[0].Action[0].(*types.StoragePlacementAction).Destination
516
517	var mds mo.Datastore
518	err = property.DefaultCollector(cmd.Client).RetrieveOne(ctx, ds, []string{"name"}, &mds)
519	if err != nil {
520		return nil, err
521	}
522
523	datastore := object.NewDatastore(cmd.Client, ds)
524	datastore.InventoryPath = mds.Name
525
526	// Apply recommendation to eligible disks
527	for _, disk := range disks {
528		backing := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo)
529		backing.Datastore = &ds
530	}
531
532	return datastore, nil
533}
534