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