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