1// Package vsphere provides node discovery for VMware vSphere.
2//
3// The package performs discovery by searching vCenter for all nodes matching a
4// certain tag, it then discovers all known IP addresses through VMware tools
5// that are not loopback or auto-configuration addresses.
6//
7// This package requires at least vSphere 6.0 in order to function.
8package vsphere
9
10import (
11	"context"
12	"fmt"
13	"io/ioutil"
14	"log"
15	"net"
16	"net/url"
17	"os"
18	"strconv"
19	"strings"
20	"time"
21
22	"github.com/hashicorp/vic/pkg/vsphere/tags"
23	"github.com/vmware/govmomi"
24	"github.com/vmware/govmomi/find"
25	"github.com/vmware/govmomi/object"
26	"github.com/vmware/govmomi/vim25/mo"
27	"github.com/vmware/govmomi/vim25/types"
28)
29
30// providerLog is the local provider logger. This should be initialized from
31// the provider entry point.
32var logger *log.Logger
33
34// setLog sets the logger.
35func setLog(l *log.Logger) {
36	if l != nil {
37		logger = l
38	} else {
39		logger = log.New(ioutil.Discard, "", 0)
40	}
41}
42
43// discoverErr prints out a friendly error heading for the top-level discovery
44// errors. It should only be used in the Addrs method.
45func discoverErr(format string, a ...interface{}) error {
46	var s string
47	if len(a) > 1 {
48		s = fmt.Sprintf(format, a...)
49	} else {
50		s = format
51	}
52	return fmt.Errorf("discover-vsphere: %s", s)
53}
54
55// valueOrEnv provides a way of suppling configuration values through
56// environment variables. Defined values always take priority.
57func valueOrEnv(config map[string]string, key, env string) string {
58	if v := config[key]; v != "" {
59		return v
60	}
61	if v := os.Getenv(env); v != "" {
62		logger.Printf("[DEBUG] Using value of %s for configuration of %s", env, key)
63		return v
64	}
65	return ""
66}
67
68// vSphereClient is a client connection manager for the vSphere provider.
69type vSphereClient struct {
70	// The VIM/govmomi client.
71	VimClient *govmomi.Client
72
73	// The specialized tags client SDK imported from vmware/vic.
74	TagsClient *tags.RestClient
75}
76
77// vimURL returns a URL to pass to the VIM SOAP client.
78func vimURL(server, user, password string) (*url.URL, error) {
79	u, err := url.Parse("https://" + server + "/sdk")
80	if err != nil {
81		return nil, fmt.Errorf("error parsing url: %s", err)
82	}
83
84	u.User = url.UserPassword(user, password)
85
86	return u, nil
87}
88
89// newVSphereClient returns a new vSphereClient after setting up the necessary
90// connections.
91func newVSphereClient(ctx context.Context, host, user, password string, insecure bool) (*vSphereClient, error) {
92	logger.Println("[DEBUG] Connecting to vSphere client endpoints")
93
94	client := new(vSphereClient)
95
96	u, err := vimURL(host, user, password)
97	if err != nil {
98		return nil, fmt.Errorf("error generating SOAP endpoint url: %s", err)
99	}
100
101	// Set up the VIM/govmomi client connection
102	client.VimClient, err = newVimSession(ctx, u, insecure)
103	if err != nil {
104		return nil, err
105	}
106
107	client.TagsClient, err = newRestSession(ctx, u, insecure)
108	if err != nil {
109		return nil, err
110	}
111
112	logger.Println("[DEBUG] All vSphere client endpoints connected successfully")
113	return client, nil
114}
115
116// newVimSession connects the VIM SOAP API client connection.
117func newVimSession(ctx context.Context, u *url.URL, insecure bool) (*govmomi.Client, error) {
118	logger.Printf("[DEBUG] Creating new SOAP API session on endpoint %s", u.Host)
119	client, err := govmomi.NewClient(ctx, u, insecure)
120	if err != nil {
121		return nil, fmt.Errorf("error setting up new vSphere SOAP client: %s", err)
122	}
123
124	logger.Println("[DEBUG] SOAP API session creation successful")
125	return client, nil
126}
127
128// newRestSession connects to the vSphere REST API endpoint, necessary for
129// tags.
130func newRestSession(ctx context.Context, u *url.URL, insecure bool) (*tags.RestClient, error) {
131	logger.Printf("[DEBUG] Creating new CIS REST API session on endpoint %s", u.Host)
132	client := tags.NewClient(u, insecure, "")
133	if err := client.Login(ctx); err != nil {
134		return nil, fmt.Errorf("error connecting to CIS REST endpoint: %s", err)
135	}
136
137	logger.Println("[DEBUG] CIS REST API session creation successful")
138	return client, nil
139}
140
141// Provider defines the vSphere discovery provider.
142type Provider struct{}
143
144// Help implements the Provider interface for the vsphere package.
145func (p *Provider) Help() string {
146	return `VMware vSphere:
147
148    provider:      "vsphere"
149    tag_name:      The name of the tag to look up.
150    category_name: The category of the tag to look up.
151    host:          The host of the vSphere server to connect to.
152    user:          The username to connect as.
153    password:      The password of the user to connect to vSphere as.
154    insecure_ssl:  Whether or not to skip SSL certificate validation.
155    timeout:       Discovery context timeout (default: 10m)
156`
157}
158
159// Addrs implements the Provider interface for the vsphere package.
160func (p *Provider) Addrs(args map[string]string, l *log.Logger) ([]string, error) {
161	if args["provider"] != "vsphere" {
162		return nil, discoverErr("invalid provider %s", args["provider"])
163	}
164
165	setLog(l)
166
167	tagName := args["tag_name"]
168	categoryName := args["category_name"]
169	host := valueOrEnv(args, "host", "VSPHERE_SERVER")
170	user := valueOrEnv(args, "user", "VSPHERE_USER")
171	password := valueOrEnv(args, "password", "VSPHERE_PASSWORD")
172	insecure, err := strconv.ParseBool(valueOrEnv(args, "insecure_ssl", "VSPHERE_ALLOW_UNVERIFIED_SSL"))
173	if err != nil {
174		logger.Println("[DEBUG] Non-truthy/falsey value for insecure_ssl, assuming false")
175	}
176	timeout, err := time.ParseDuration(args["timeout"])
177	if err != nil {
178		logger.Println("[DEBUG] Non-time value given for timeout, assuming 10m")
179		timeout = time.Minute * 10
180	}
181
182	ctx, cancel := context.WithTimeout(context.Background(), timeout)
183	defer cancel()
184
185	client, err := newVSphereClient(ctx, host, user, password, insecure)
186	if err != nil {
187		return nil, discoverErr(err.Error())
188	}
189
190	if tagName == "" || categoryName == "" {
191		return nil, discoverErr("both tag_name and category_name must be specified")
192	}
193
194	logger.Printf("[INFO] Locating all virtual machine IP addresses with tag %q in category %q", tagName, categoryName)
195
196	tagID, err := tagIDFromName(ctx, client.TagsClient, tagName, categoryName)
197	if err != nil {
198		return nil, discoverErr(err.Error())
199	}
200
201	addrs, err := virtualMachineIPsForTag(ctx, client, tagID)
202	if err != nil {
203		return nil, discoverErr(err.Error())
204	}
205
206	logger.Printf("[INFO] Final IP address list: %s", strings.Join(addrs, ","))
207	return addrs, nil
208}
209
210// tagIDFromName helps convert the tag and category names into the final ID
211// used for discovery.
212func tagIDFromName(ctx context.Context, client *tags.RestClient, name, category string) (string, error) {
213	logger.Printf("[DEBUG] Fetching tag ID for tag name %q and category %q", name, category)
214
215	categoryID, err := tagCategoryByName(ctx, client, category)
216	if err != nil {
217		return "", err
218	}
219
220	return tagByName(ctx, client, name, categoryID)
221}
222
223// tagCategoryByName converts a tag category name into its ID.
224func tagCategoryByName(ctx context.Context, client *tags.RestClient, name string) (string, error) {
225	cats, err := client.GetCategoriesByName(ctx, name)
226	if err != nil {
227		return "", fmt.Errorf("could not get category for name %q: %s", name, err)
228	}
229
230	if len(cats) < 1 {
231		return "", fmt.Errorf("category name %q not found", name)
232	}
233	if len(cats) > 1 {
234		// Although GetCategoriesByName does not seem to think that tag categories
235		// are unique, empirical observation via the console and API show that they
236		// are. This error case is handled anyway.
237		return "", fmt.Errorf("multiple categories with name %q found", name)
238	}
239
240	return cats[0].ID, nil
241}
242
243// tagByName converts a tag name into its ID.
244func tagByName(ctx context.Context, client *tags.RestClient, name, categoryID string) (string, error) {
245	tids, err := client.GetTagByNameForCategory(ctx, name, categoryID)
246	if err != nil {
247		return "", fmt.Errorf("could not get tag for name %q: %s", name, err)
248	}
249
250	if len(tids) < 1 {
251		return "", fmt.Errorf("tag name %q not found in category ID %q", name, categoryID)
252	}
253	if len(tids) > 1 {
254		// This situation is very similar to the one in tagCategoryByName. The API
255		// docs even say that tags need to be unique in categories, yet
256		// GetTagByNameForCategory still returns multiple results.
257		return "", fmt.Errorf("multiple tags with name %q found", name)
258	}
259
260	logger.Printf("[DEBUG] Tag ID is %q", tids[0].ID)
261	return tids[0].ID, nil
262}
263
264// virtualMachineIPsForTag is a higher-level wrapper that calls out to
265// functions to fetch all of the virtual machines matching a certain tag ID,
266// and then gets all of the IP addresses for those virtual machines.
267func virtualMachineIPsForTag(ctx context.Context, client *vSphereClient, id string) ([]string, error) {
268	vms, err := virtualMachinesForTag(ctx, client, id)
269	if err != nil {
270		return nil, err
271	}
272
273	return ipAddrsForVirtualMachines(ctx, client, vms)
274}
275
276// virtualMachinesForTag discovers all of the virtual machines that match a
277// specific tag ID and returns their higher level helper objects.
278func virtualMachinesForTag(ctx context.Context, client *vSphereClient, id string) ([]*object.VirtualMachine, error) {
279	logger.Printf("[DEBUG] Locating all virtual machines under tag ID %q", id)
280
281	var vms []*object.VirtualMachine
282
283	objs, err := client.TagsClient.ListAttachedObjects(ctx, id)
284	if err != nil {
285		return nil, err
286	}
287	for i, obj := range objs {
288		switch {
289		case obj.Type == nil || obj.ID == nil:
290			logger.Printf("[WARN] Discovered object at index %d has either no ID or type", i)
291			continue
292		case *obj.Type != "VirtualMachine":
293			logger.Printf("[DEBUG] Discovered object ID %q is not a virutal machine", *obj.ID)
294			continue
295		}
296		vm, err := virtualMachineFromMOID(ctx, client.VimClient, *obj.ID)
297		if err != nil {
298			return nil, fmt.Errorf("error locating virtual machine with ID %q: %s", *obj.ID, err)
299		}
300		vms = append(vms, vm)
301	}
302
303	logger.Printf("[DEBUG] Discovered virtual machines: %s", virtualMachineNames(vms))
304	return vms, nil
305}
306
307// ipAddrsForVirtualMachines takes a set of virtual machines and returns a
308// consolidated list of IP addresses for all of the VMs.
309func ipAddrsForVirtualMachines(ctx context.Context, client *vSphereClient, vms []*object.VirtualMachine) ([]string, error) {
310	var addrs []string
311	for _, vm := range vms {
312		as, err := buildAndSelectGuestIPs(ctx, vm)
313		if err != nil {
314			return nil, err
315		}
316		addrs = append(addrs, as...)
317	}
318	return addrs, nil
319}
320
321// virtualMachineFromMOID locates a virtual machine by its managed object
322// reference ID.
323func virtualMachineFromMOID(ctx context.Context, client *govmomi.Client, id string) (*object.VirtualMachine, error) {
324	logger.Printf("[DEBUG] Locating VM with managed object ID %q", id)
325
326	finder := find.NewFinder(client.Client, false)
327
328	ref := types.ManagedObjectReference{
329		Type:  "VirtualMachine",
330		Value: id,
331	}
332
333	vm, err := finder.ObjectReference(ctx, ref)
334	if err != nil {
335		return nil, err
336	}
337	// Should be safe to return here. If our reference returned here and is not a
338	// VM, then we have bigger problems and to be honest we should be panicking
339	// anyway.
340	return vm.(*object.VirtualMachine), nil
341}
342
343// virtualMachineProperties is a convenience method that wraps fetching the
344// VirtualMachine MO from its higher-level object.
345//
346// It takes a list of property keys to fetch. Keeping the property set small
347// can sometimes result in significant performance improvements.
348func virtualMachineProperties(ctx context.Context, vm *object.VirtualMachine, keys []string) (*mo.VirtualMachine, error) {
349	logger.Printf("[DEBUG] Fetching properties for VM %q", vm.Name())
350	var props mo.VirtualMachine
351	if err := vm.Properties(ctx, vm.Reference(), keys, &props); err != nil {
352		return nil, err
353	}
354	return &props, nil
355}
356
357// buildAndSelectGuestIPs builds a list of IP addresses known to VMware tools,
358// skipping local and auto-configuration addresses.
359//
360// The builder is non-discriminate and is only deterministic to the order that
361// it discovers addresses in VMware tools.
362func buildAndSelectGuestIPs(ctx context.Context, vm *object.VirtualMachine) ([]string, error) {
363	logger.Printf("[DEBUG] Discovering addresses for virtual machine %q", vm.Name())
364	var addrs []string
365
366	props, err := virtualMachineProperties(ctx, vm, []string{"guest.net"})
367	if err != nil {
368		return nil, fmt.Errorf("cannot fetch properties for VM %q: %s", vm.Name(), err)
369	}
370
371	if props.Guest == nil || props.Guest.Net == nil {
372		logger.Printf("[WARN] No networking stack information available for %q or VMware tools not running", vm.Name())
373		return nil, nil
374	}
375
376	// Now fetch all IP addresses, checking at the same time to see if the IP
377	// address is eligible to be a primary IP address.
378	for _, n := range props.Guest.Net {
379		if n.IpConfig != nil {
380			for _, addr := range n.IpConfig.IpAddress {
381				if skipIPAddr(net.ParseIP(addr.IpAddress)) {
382					continue
383				}
384				addrs = append(addrs, addr.IpAddress)
385			}
386		}
387	}
388
389	logger.Printf("[INFO] Discovered IP addresses for virtual machine %q: %s", vm.Name(), strings.Join(addrs, ","))
390	return addrs, nil
391}
392
393// skipIPAddr defines the set of criteria that buildAndSelectGuestIPs uses to
394// check to see if it needs to skip an IP address.
395func skipIPAddr(ip net.IP) bool {
396	switch {
397	case ip.IsLinkLocalMulticast():
398		fallthrough
399	case ip.IsLinkLocalUnicast():
400		fallthrough
401	case ip.IsLoopback():
402		fallthrough
403	case ip.IsMulticast():
404		return true
405	}
406	return false
407}
408
409// virtualMachineNames is a helper method that returns all the names for a list
410// of virtual machines, comma separated.
411func virtualMachineNames(vms []*object.VirtualMachine) string {
412	var s []string
413	for _, vm := range vms {
414		s = append(s, vm.Name())
415	}
416	return strings.Join(s, ",")
417}
418