1// Copyright 2016-2018 VMware, Inc. All Rights Reserved.
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
15package management
16
17import (
18	"fmt"
19	"path"
20
21	"github.com/vmware/govmomi/find"
22	"github.com/vmware/govmomi/object"
23	"github.com/vmware/govmomi/vim25/types"
24	"github.com/vmware/vic/lib/config"
25	"github.com/vmware/vic/lib/install/validate"
26	"github.com/vmware/vic/lib/migration"
27	"github.com/vmware/vic/pkg/errors"
28	"github.com/vmware/vic/pkg/retry"
29	"github.com/vmware/vic/pkg/trace"
30	"github.com/vmware/vic/pkg/vsphere/compute"
31	"github.com/vmware/vic/pkg/vsphere/extraconfig"
32	"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
33	"github.com/vmware/vic/pkg/vsphere/tasks"
34	"github.com/vmware/vic/pkg/vsphere/vm"
35)
36
37const (
38	vchIDType = "VirtualMachine"
39)
40
41func (d *Dispatcher) NewVCHFromID(id string) (*vm.VirtualMachine, error) {
42	defer trace.End(trace.Begin(id, d.op))
43
44	var err error
45
46	moref := &types.ManagedObjectReference{
47		Type:  vchIDType,
48		Value: id,
49	}
50	ref, err := d.session.Finder.ObjectReference(d.op, *moref)
51	if err != nil {
52		if !isManagedObjectNotFoundError(err) {
53			err = errors.Errorf("Failed to query appliance (%q): %s", moref, err)
54			return nil, err
55		}
56		d.op.Debug("Appliance is not found")
57		return nil, fmt.Errorf("id %q could not be found", id)
58	}
59	ovm, ok := ref.(*object.VirtualMachine)
60	if !ok {
61		d.op.Errorf("Failed to find VM %q: %s", moref, err)
62		return nil, err
63	}
64	d.appliance = vm.NewVirtualMachine(d.op, d.session, ovm.Reference())
65
66	// check if it's VCH
67	if ok, err = d.isVCH(d.appliance); err != nil {
68		d.op.Error(err)
69		return nil, err
70	}
71	if !ok {
72		err = errors.Errorf("Not a VCH")
73		d.op.Error(err)
74		return nil, err
75	}
76	d.vchPool, err = d.appliance.ResourcePool(d.op)
77	if err != nil {
78		d.op.Errorf("Failed to get VM parent resource pool: %s", err)
79		return nil, err
80	}
81
82	rp := compute.NewResourcePool(d.op, d.session, d.vchPool.Reference())
83	if d.session.Cluster, err = rp.GetCluster(d.op); err != nil {
84		d.op.Debugf("Unable to get the cluster for the VCH's resource pool: %s", err)
85	}
86	d.InitDiagnosticLogsFromVCH(d.appliance)
87	return d.appliance, nil
88}
89
90func (d *Dispatcher) NewVCHFromComputePath(computePath string, name string, v *validate.Validator) (*vm.VirtualMachine, error) {
91	defer trace.End(trace.Begin(fmt.Sprintf("path %q, name %q", computePath, name), d.op))
92
93	var err error
94
95	parent, err := v.ResourcePoolHelper(d.op, computePath)
96	if err != nil {
97		return nil, err
98	}
99	d.vchPoolPath = path.Join(parent.InventoryPath, name)
100	if d.isVC {
101		vapp, err := d.findVirtualApp(d.vchPoolPath)
102		if err != nil {
103			d.op.Errorf("Failed to get VCH virtual app %q: %s", d.vchPoolPath, err)
104			return nil, err
105		}
106		if vapp != nil {
107			d.vchPool = vapp.ResourcePool
108		}
109	}
110	if d.vchPool == nil {
111		d.vchPool, err = d.session.Finder.ResourcePool(d.op, d.vchPoolPath)
112		if err != nil {
113			// we didn't find the ResourcePool with a name matching the appliance, so
114			// lets look for just the resource pool -- this could be at the cluster level
115			d.vchPoolPath = parent.InventoryPath
116			d.vchPool, err = d.session.Finder.ResourcePool(d.op, d.vchPoolPath)
117			if err != nil {
118				d.op.Errorf("Failed to find VCH resource pool %q: %s", d.vchPoolPath, err)
119				return nil, err
120			}
121		}
122	}
123
124	// creating a pkg/vsphere resource pool for use of convenience method
125	rp := compute.NewResourcePool(d.op, d.session, d.vchPool.Reference())
126
127	if d.session.Cluster, err = rp.GetCluster(d.op); err != nil {
128		d.op.Debugf("Unable to get the cluster for the VCH's resource pool: %s", err)
129	}
130
131	if d.appliance, err = rp.GetChildVM(d.op, name); err != nil {
132		d.op.Errorf("Failed to get VCH VM: %s", err)
133		return nil, err
134	}
135	if d.appliance == nil {
136		err = errors.Errorf("Didn't find VM %q in resource pool %q", name, rp.Reference())
137		d.op.Error(err)
138		return nil, err
139	}
140	d.appliance.InventoryPath = path.Join(d.vchPoolPath, name)
141
142	// check if it's VCH
143	var ok bool
144	if ok, err = d.isVCH(d.appliance); err != nil {
145		d.op.Error(err)
146		return nil, err
147	}
148	if !ok {
149		err = errors.Errorf("Not a VCH")
150		d.op.Error(err)
151		return nil, err
152	}
153
154	d.InitDiagnosticLogsFromVCH(d.appliance)
155	return d.appliance, nil
156}
157
158// GetVCHConfig queries VCH configuration and decrypts secret information
159func (d *Dispatcher) GetVCHConfig(vm *vm.VirtualMachine) (*config.VirtualContainerHostConfigSpec, error) {
160	defer trace.End(trace.Begin("", d.op))
161
162	//this is the appliance vm
163	mapConfig, err := vm.FetchExtraConfigBaseOptions(d.op)
164	if err != nil {
165		err = errors.Errorf("Failed to get VM extra config of %q: %s", vm.Reference(), err)
166		d.op.Error(err)
167		return nil, err
168	}
169
170	kv := vmomi.OptionValueMap(mapConfig)
171	vchConfig, err := d.decryptVCHConfig(vm, kv)
172	if err != nil {
173		err = errors.Errorf("Failed to decode VM configuration %q: %s", vm.Reference(), err)
174		d.op.Error(err)
175		return nil, err
176	}
177
178	if vchConfig.IsCreating() {
179		vmRef := vm.Reference()
180		vchConfig.SetMoref(&vmRef)
181	}
182	return vchConfig, nil
183}
184
185// GetNoSecretVCHConfig queries vch configure from vm configuration, without decrypting secret information
186// this method is used to accommodate old vch version without secret information
187func (d *Dispatcher) GetNoSecretVCHConfig(vm *vm.VirtualMachine) (*config.VirtualContainerHostConfigSpec, error) {
188	defer trace.End(trace.Begin("", d.op))
189
190	//this is the appliance vm
191	mapConfig, err := vm.FetchExtraConfigBaseOptions(d.op)
192	if err != nil {
193		err = errors.Errorf("Failed to get VM extra config of %q: %s", vm.Reference(), err)
194		d.op.Error(err)
195		return nil, err
196	}
197
198	kv := vmomi.OptionValueMap(mapConfig)
199	vchConfig := &config.VirtualContainerHostConfigSpec{}
200	extraconfig.Decode(extraconfig.MapSource(kv), vchConfig)
201
202	if vchConfig.IsCreating() {
203		vmRef := vm.Reference()
204		vchConfig.SetMoref(&vmRef)
205	}
206	return vchConfig, nil
207}
208
209// FetchAndMigrateVCHConfig queries VCH guestinfo, and try to migrate older version data to latest if the data is old
210func (d *Dispatcher) FetchAndMigrateVCHConfig(vm *vm.VirtualMachine) (*config.VirtualContainerHostConfigSpec, error) {
211	defer trace.End(trace.Begin("", d.op))
212
213	//this is the appliance vm
214	mapConfig, err := vm.FetchExtraConfigBaseOptions(d.op)
215	if err != nil {
216		err = errors.Errorf("Failed to get VM extra config of %q: %s", vm.Reference(), err)
217		return nil, err
218	}
219
220	kv := vmomi.OptionValueMap(mapConfig)
221	newMap, migrated, err := migration.MigrateApplianceConfig(d.op, d.session, kv)
222	if err != nil {
223		err = errors.Errorf("Failed to migrate config of %q: %s", vm.Reference(), err)
224		return nil, err
225	}
226	if !migrated {
227		d.op.Debugf("No need to migrate configuration for %q", vm.Reference())
228	}
229	return d.decryptVCHConfig(vm, newMap)
230}
231
232// SearchVCHs searches for VCHs in one of the following areas:
233//
234// computePath:  if a compute resource is provided then the search will be limited
235// to the resource pools of the compute resources found at the path
236//
237// datacenter:  if no compute resource is provided then search across all compute
238// resources in the sessions datacenter.  This requires the user to specify the datacenter
239// via the target flag
240//
241// all datacenters:  if the other options aren't available then search across every compute
242// resource in each vSphere datacenter
243func (d *Dispatcher) SearchVCHs(computePath string) ([]*vm.VirtualMachine, error) {
244	defer trace.End(trace.Begin(computePath, d.op))
245	if computePath != "" {
246		return d.search(computePath)
247	}
248	if d.session.Datacenter != nil {
249		return d.search(path.Join(d.session.Datacenter.InventoryPath, "..."))
250	}
251
252	dcs, err := d.session.Finder.DatacenterList(d.op, "*")
253	if err != nil {
254		err = errors.Errorf("Failed to get datacenter list: %s", err)
255		return nil, err
256	}
257
258	var vchs []*vm.VirtualMachine
259	for _, dc := range dcs {
260		d.session.Finder.SetDatacenter(dc)
261		dcVCHs, err := d.search(path.Join(dc.InventoryPath, "..."))
262		if err != nil {
263			// we will just warn for now
264			d.op.Warnf("Error searching the datacenter(%s): %s", dc.Name(), err)
265			continue
266		}
267		vchs = append(vchs, dcVCHs...)
268	}
269	return vchs, nil
270}
271
272// search finds the compute resources based on the search path and then retrieves
273// the resouce pools for that compute resource.  Once a list of resource pools is
274// collected search will iterate over the pools and search for VCHs in each pool
275func (d *Dispatcher) search(searchPath string) ([]*vm.VirtualMachine, error) {
276	defer trace.End(trace.Begin(searchPath, d.op))
277	var vchs []*vm.VirtualMachine
278	// find compute resources for the search path
279	resources, err := d.session.Finder.ComputeResourceList(d.op, searchPath)
280	if err != nil {
281		if _, ok := err.(*find.NotFoundError); ok {
282			return nil, nil
283		}
284		return nil, err
285	}
286
287	// iterate over clusters and search each resourcePool in the cluster
288	for _, c := range resources {
289		pools, err := d.listResourcePools(c.InventoryPath)
290		if err != nil {
291			e := fmt.Errorf("Unable to find resource pools for compute(%s): %s", c.Name(), err)
292			return nil, e
293		}
294		// now search the pools for VCHs
295		vms := d.searchResourcePools(pools)
296		vchs = append(vchs, vms...)
297	}
298	return vchs, nil
299}
300
301// listResourcePool returns a list of all resource pools under a compute path
302// The func will rety on an ObjectNotFound error because sometimes it's due to concurrent
303// operations on the resource pool
304func (d *Dispatcher) listResourcePools(searchPath string) ([]*object.ResourcePool, error) {
305	var err error
306	var pools []*object.ResourcePool
307
308	// under some circumstances, such as when there is concurrent vic-machine delete operation running in the background,
309	// listing resource pools might fail because some VCH pool is being destroyed at the same time.
310	// If that happens, we retry and list pools again
311	err = retry.Do(d.op, func() error {
312		pools, err = d.session.Finder.ResourcePoolList(d.op, path.Join(searchPath, "..."))
313		if _, ok := err.(*find.NotFoundError); ok {
314			return nil
315		}
316		return err
317	}, func(err error) bool {
318		return tasks.IsTransientError(d.op, err) || tasks.IsNotFoundError(err)
319	})
320
321	return pools, err
322}
323
324// searchResourcePools searches for VCHs in the provided list of resource pools
325func (d *Dispatcher) searchResourcePools(pools []*object.ResourcePool) []*vm.VirtualMachine {
326	var vchs []*vm.VirtualMachine
327	// The number of resource pools found for the compute resource determines how
328	// to search for any VCHs.  If there is only one pool then look at all VMs in the
329	// resource pool for VCH metadata.  If there are multiple pools then look only at VMs
330	// that have the same name as the resource pool.
331	multiPool := false
332	if len(pools) > 1 {
333		multiPool = true
334	}
335	// iterate over the compute resources pools and search for VMs and vApps
336	for _, pool := range pools {
337		children, err := d.findVCHs(pool, multiPool)
338		// #nosec: Errors unhandled.
339		if err != nil {
340			d.op.Warnf("Failed to get VCH from resource pool %q: %s", pool.InventoryPath, err)
341		} else {
342			vchs = append(vchs, children...)
343		}
344
345		// search for a vApp
346		vappPath := path.Join(pool.InventoryPath, "*")
347		vapps, err := d.session.Finder.VirtualAppList(d.op, vappPath)
348		if err != nil {
349			if _, ok := err.(*find.NotFoundError); !ok {
350				d.op.Errorf("Failed to query vapp %q: %s", vappPath, err)
351			}
352		}
353		for _, vapp := range vapps {
354			children, err := d.findVCHs(vapp.ResourcePool, multiPool)
355			if err != nil {
356				d.op.Warnf("Failed to get VCH from vApp resource pool %q: %s", pool.InventoryPath, err)
357				continue
358			}
359			vchs = append(vchs, children...)
360		}
361
362	}
363	return vchs
364}
365
366// findVCHs finds any VCH in the specified pool.  If the compute resource has multiple pools, then look
367// for any VMs with the same name as the resource pool.  If the compute resource only had a single pool then
368// evaluate each VM in the pool for VCH metadata
369func (d *Dispatcher) findVCHs(pool *object.ResourcePool, multiPool bool) ([]*vm.VirtualMachine, error) {
370	defer trace.End(trace.Begin(pool.InventoryPath, d.op))
371
372	// check if pool itself contains VCH vm.
373	var vms []*vm.VirtualMachine
374	var vchs []*vm.VirtualMachine
375	var err error
376	computeResource := compute.NewResourcePool(d.op, d.session, pool.Reference())
377	// The compute resource had multiple pools, so the assumption is that any VCH that exists will be the same
378	// name as it's resource pool
379	if multiPool {
380		vm, err := computeResource.GetChildVM(d.op, pool.Name())
381		if err != nil {
382			return nil, errors.Errorf("Failed to query children VM in resource pool %q: %s", pool.InventoryPath, err)
383		}
384		if vm != nil {
385			vms = append(vms, vm)
386		}
387
388	} else {
389		// We only had one pool, so we'll look at all the VMs in that pool
390		vms, err = computeResource.GetChildrenVMs(d.op)
391		if err != nil {
392			return nil, errors.Errorf("Failed to query children VM in resource pool %q: %s", pool.InventoryPath, err)
393		}
394	}
395	// iterate over VMs and determine if we've got any VCHs
396	for _, v := range vms {
397		// override the VM inventory path (which is folder based)
398		// for the resource pool path
399		v.InventoryPath = path.Join(pool.InventoryPath, v.Name())
400		// #nosec: Errors unhandled.
401		if ok, _ := d.isVCH(v); ok {
402			d.op.Debugf("%q is VCH", v.InventoryPath)
403			vchs = append(vchs, v)
404		}
405	}
406
407	return vchs, nil
408}
409