From 00f0dbaae34ee4523400c6f6bb5a6d3be32382e4 Mon Sep 17 00:00:00 2001 From: brian57860 <53904715+brian57860@users.noreply.github.com> Date: Tue, 13 Aug 2019 11:41:15 +0100 Subject: [PATCH] Added Instant Clone feature Resolves: #1392 --- govc/USAGE.md | 45 +++++++ govc/flags/datastore.go | 4 + govc/flags/folder.go | 4 + govc/flags/resource_pool.go | 4 + govc/vm/instantclone.go | 245 ++++++++++++++++++++++++++++++++++++ object/virtual_machine.go | 14 +++ 6 files changed, 316 insertions(+) create mode 100644 govc/vm/instantclone.go diff --git a/govc/USAGE.md b/govc/USAGE.md index cc322c3b7..ddd796c7b 100644 --- a/govc/USAGE.md +++ b/govc/USAGE.md @@ -270,6 +270,7 @@ but appear via `govc $cmd -h`: - [vm.disk.create](#vmdiskcreate) - [vm.guest.tools](#vmguesttools) - [vm.info](#vminfo) + - [vm.instantclone](#vminstantclone) - [vm.ip](#vmip) - [vm.keystrokes](#vmkeystrokes) - [vm.markastemplate](#vmmarkastemplate) @@ -4438,6 +4439,50 @@ Options: -waitip=false Wait for VM to acquire IP address ``` +## vm.instantclone + +``` +Usage: govc vm.instantclone [OPTIONS] NAME + +Instant Clone VM to NAME. + +Examples: + govc vm.instantclone -vm source-vm new-vm + # Configure ExtraConfig variables on a guest VM: + govc vm.instantclone -vm source-vm -e guestinfo.ipaddress=192.168.0.1 -e guestinfo.netmask=255.255.255.0 new-vm + # Read the variable set above inside the guest: + vmware-rpctool "info-get guestinfo.ipaddress" + vmware-rpctool "info-get guestinfo.netmask" + +Options: + -cert= Certificate [GOVC_CERTIFICATE] + -dc= Datacenter [GOVC_DATACENTER] + -debug=false Store debug logs [GOVC_DEBUG] + -ds= Datastore [GOVC_DATASTORE] + -dump=false Enable Go output + -e=[] ExtraConfig. = + -folder= Inventory folder [GOVC_FOLDER] + -json=false Enable JSON output + -k=false Skip verification of server certificate [GOVC_INSECURE] + -key= Private key [GOVC_PRIVATE_KEY] + -net= Network [GOVC_NETWORK] + -net.adapter=e1000 Network adapter type + -net.address= Network hardware address + -persist-session=true Persist session to disk [GOVC_PERSIST_SESSION] + -pool= Resource pool [GOVC_RESOURCE_POOL] + -tls-ca-certs= TLS CA certificates file [GOVC_TLS_CA_CERTS] + -tls-known-hosts= TLS known hosts file [GOVC_TLS_KNOWN_HOSTS] + -u= ESX or vCenter URL [GOVC_URL] + -vim-namespace=vim25 Vim namespace [GOVC_VIM_NAMESPACE] + -vim-version=6.7 Vim version [GOVC_VIM_VERSION] + -vm= Virtual machine [GOVC_VM] + -vm.dns= Find VM by FQDN + -vm.ip= Find VM by IP address + -vm.ipath= Find VM by inventory path + -vm.path= Find VM by path to .vmx file + -vm.uuid= Find VM by UUID +``` + ## vm.ip ``` diff --git a/govc/flags/datastore.go b/govc/flags/datastore.go index bbe7e8c7d..8030a6e00 100644 --- a/govc/flags/datastore.go +++ b/govc/flags/datastore.go @@ -76,6 +76,10 @@ func (f *DatastoreFlag) Process(ctx context.Context) error { }) } +func (flag *DatastoreFlag) IsSet() bool { + return flag.Name != "" +} + func (f *DatastoreFlag) Args(args []string) []object.DatastorePath { var files []object.DatastorePath diff --git a/govc/flags/folder.go b/govc/flags/folder.go index 2617ae762..d23522fab 100644 --- a/govc/flags/folder.go +++ b/govc/flags/folder.go @@ -67,6 +67,10 @@ func (flag *FolderFlag) Process(ctx context.Context) error { }) } +func (flag *FolderFlag) IsSet() bool { + return flag.name != "" +} + func (flag *FolderFlag) Folder() (*object.Folder, error) { if flag.folder != nil { return flag.folder, nil diff --git a/govc/flags/resource_pool.go b/govc/flags/resource_pool.go index 38379654e..534cca222 100644 --- a/govc/flags/resource_pool.go +++ b/govc/flags/resource_pool.go @@ -67,6 +67,10 @@ func (flag *ResourcePoolFlag) Process(ctx context.Context) error { }) } +func (flag *ResourcePoolFlag) IsSet() bool { + return flag.name != "" +} + func (flag *ResourcePoolFlag) ResourcePool() (*object.ResourcePool, error) { if flag.pool != nil { return flag.pool, nil diff --git a/govc/vm/instantclone.go b/govc/vm/instantclone.go new file mode 100644 index 000000000..c6f3b9ebb --- /dev/null +++ b/govc/vm/instantclone.go @@ -0,0 +1,245 @@ +/* +Copyright (c) 2014-2016 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "context" + "flag" + "fmt" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/types" +) + +type instantclone struct { + *flags.ClientFlag + *flags.DatacenterFlag + *flags.DatastoreFlag + *flags.ResourcePoolFlag + *flags.NetworkFlag + *flags.FolderFlag + *flags.VirtualMachineFlag + + name string + extraConfig extraConfig + + Client *vim25.Client + Datacenter *object.Datacenter + Datastore *object.Datastore + ResourcePool *object.ResourcePool + Folder *object.Folder + VirtualMachine *object.VirtualMachine +} + +func init() { + cli.Register("vm.instantclone", &instantclone{}) +} + +func (cmd *instantclone) Register(ctx context.Context, f *flag.FlagSet) { + cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) + cmd.ClientFlag.Register(ctx, f) + + cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx) + cmd.DatacenterFlag.Register(ctx, f) + + cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx) + cmd.DatastoreFlag.Register(ctx, f) + + cmd.ResourcePoolFlag, ctx = flags.NewResourcePoolFlag(ctx) + cmd.ResourcePoolFlag.Register(ctx, f) + + cmd.NetworkFlag, ctx = flags.NewNetworkFlag(ctx) + cmd.NetworkFlag.Register(ctx, f) + + cmd.FolderFlag, ctx = flags.NewFolderFlag(ctx) + cmd.FolderFlag.Register(ctx, f) + + cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx) + cmd.VirtualMachineFlag.Register(ctx, f) + + f.Var(&cmd.extraConfig, "e", "ExtraConfig. =") +} + +func (cmd *instantclone) Usage() string { + return "NAME" +} + +func (cmd *instantclone) Description() string { + return `Instant Clone VM to NAME. + +Examples: + govc vm.instantclone -vm source-vm new-vm + # Configure ExtraConfig variables on a guest VM: + govc vm.instantclone -vm source-vm -e guestinfo.ipaddress=192.168.0.1 -e guestinfo.netmask=255.255.255.0 new-vm + # Read the variable set above inside the guest: + vmware-rpctool "info-get guestinfo.ipaddress" + vmware-rpctool "info-get guestinfo.netmask"` +} + +func (cmd *instantclone) Process(ctx context.Context) error { + if err := cmd.ClientFlag.Process(ctx); err != nil { + return err + } + if err := cmd.DatacenterFlag.Process(ctx); err != nil { + return err + } + if err := cmd.DatastoreFlag.Process(ctx); err != nil { + return err + } + if err := cmd.ResourcePoolFlag.Process(ctx); err != nil { + return err + } + if err := cmd.NetworkFlag.Process(ctx); err != nil { + return err + } + if err := cmd.FolderFlag.Process(ctx); err != nil { + return err + } + if err := cmd.VirtualMachineFlag.Process(ctx); err != nil { + return err + } + + return nil +} + +func (cmd *instantclone) Run(ctx context.Context, f *flag.FlagSet) error { + var err error + + if len(f.Args()) != 1 { + return flag.ErrHelp + } + + cmd.name = f.Arg(0) + if cmd.name == "" { + return flag.ErrHelp + } + + cmd.Client, err = cmd.ClientFlag.Client() + if err != nil { + return err + } + + cmd.Datacenter, err = cmd.DatacenterFlag.Datacenter() + if err != nil { + return err + } + + cmd.Datastore, err = cmd.DatastoreFlag.Datastore() + if err != nil { + return err + } + + cmd.Folder, err = cmd.FolderFlag.Folder() + if err != nil { + return err + } + + cmd.VirtualMachine, err = cmd.VirtualMachineFlag.VirtualMachine() + if err != nil { + return err + } + + if cmd.VirtualMachine == nil { + return flag.ErrHelp + } + + _, err = cmd.instantcloneVM(ctx) + if err != nil { + return err + } + + return nil +} + +func (cmd *instantclone) instantcloneVM(ctx context.Context) (*object.VirtualMachine, error) { + relocateSpec := types.VirtualMachineRelocateSpec{} + + if cmd.NetworkFlag.IsSet() { + devices, err := cmd.VirtualMachine.Device(ctx) + if err != nil { + return nil, err + } + + // prepare virtual device config spec for network card + configSpecs := []types.BaseVirtualDeviceConfigSpec{} + + op := types.VirtualDeviceConfigSpecOperationAdd + card, derr := cmd.NetworkFlag.Device() + if derr != nil { + return nil, derr + } + // search for the first network card of the source + for _, device := range devices { + if _, ok := device.(types.BaseVirtualEthernetCard); ok { + op = types.VirtualDeviceConfigSpecOperationEdit + // set new backing info + cmd.NetworkFlag.Change(device, card) + card = device + break + } + } + + configSpecs = append(configSpecs, &types.VirtualDeviceConfigSpec{ + Operation: op, + Device: card, + }) + + relocateSpec.DeviceChange = configSpecs + } + + if cmd.FolderFlag.IsSet() { + folderref := cmd.Folder.Reference() + relocateSpec.Folder = &folderref + } + + if cmd.ResourcePoolFlag.IsSet() { + poolref := cmd.ResourcePool.Reference() + relocateSpec.Pool = &poolref + } + + if cmd.DatastoreFlag.IsSet() { + datastoreref := cmd.Datastore.Reference() + relocateSpec.Datastore = &datastoreref + } + + instantcloneSpec := &types.VirtualMachineInstantCloneSpec{ + Name: cmd.name, + Location: relocateSpec, + } + + if len(cmd.extraConfig) > 0 { + instantcloneSpec.Config = cmd.extraConfig + } + + task, err := cmd.VirtualMachine.InstantClone(ctx, *instantcloneSpec) + if err != nil { + return nil, err + } + + logger := cmd.ProgressLogger(fmt.Sprintf("Instant Cloning %s to %s...", cmd.VirtualMachine.InventoryPath, cmd.name)) + defer logger.Wait() + + info, err := task.WaitForResult(ctx, logger) + if err != nil { + return nil, err + } + + return object.NewVirtualMachine(cmd.Client, info.Result.(types.ManagedObjectReference)), nil +} diff --git a/object/virtual_machine.go b/object/virtual_machine.go index 7c7f852d9..5ca31aebf 100644 --- a/object/virtual_machine.go +++ b/object/virtual_machine.go @@ -169,6 +169,20 @@ func (v VirtualMachine) Clone(ctx context.Context, folder *Folder, name string, return NewTask(v.c, res.Returnval), nil } +func (v VirtualMachine) InstantClone(ctx context.Context, config types.VirtualMachineInstantCloneSpec) (*Task, error) { + req := types.InstantClone_Task{ + This: v.Reference(), + Spec: config, + } + + res, err := methods.InstantClone_Task(ctx, v.c, &req) + if err != nil { + return nil, err + } + + return NewTask(v.c, res.Returnval), nil +} + func (v VirtualMachine) Customize(ctx context.Context, spec types.CustomizationSpec) (*Task, error) { req := types.CustomizeVM_Task{ This: v.Reference(),