github.com/vmware/govmomi@v0.51.0/object/datastore.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package object
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"math/rand"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path"
    16  	"strings"
    17  
    18  	"github.com/vmware/govmomi/internal"
    19  	"github.com/vmware/govmomi/property"
    20  	"github.com/vmware/govmomi/session"
    21  	"github.com/vmware/govmomi/vim25"
    22  	"github.com/vmware/govmomi/vim25/mo"
    23  	"github.com/vmware/govmomi/vim25/soap"
    24  	"github.com/vmware/govmomi/vim25/types"
    25  )
    26  
    27  // DatastoreNoSuchDirectoryError is returned when a directory could not be found.
    28  type DatastoreNoSuchDirectoryError struct {
    29  	verb    string
    30  	subject string
    31  }
    32  
    33  func (e DatastoreNoSuchDirectoryError) Error() string {
    34  	return fmt.Sprintf("cannot %s '%s': No such directory", e.verb, e.subject)
    35  }
    36  
    37  // DatastoreNoSuchFileError is returned when a file could not be found.
    38  type DatastoreNoSuchFileError struct {
    39  	verb    string
    40  	subject string
    41  }
    42  
    43  func (e DatastoreNoSuchFileError) Error() string {
    44  	return fmt.Sprintf("cannot %s '%s': No such file", e.verb, e.subject)
    45  }
    46  
    47  type Datastore struct {
    48  	Common
    49  
    50  	DatacenterPath string
    51  }
    52  
    53  func NewDatastore(c *vim25.Client, ref types.ManagedObjectReference) *Datastore {
    54  	return &Datastore{
    55  		Common: NewCommon(c, ref),
    56  	}
    57  }
    58  
    59  // FindInventoryPath sets InventoryPath and DatacenterPath,
    60  // needed by NewURL() to compose an upload/download endpoint URL
    61  func (d *Datastore) FindInventoryPath(ctx context.Context) error {
    62  	entities, err := mo.Ancestors(ctx, d.c, d.c.ServiceContent.PropertyCollector, d.r)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	val := "/"
    68  
    69  	for _, entity := range entities {
    70  		if entity.Parent == nil {
    71  			continue // root folder
    72  		}
    73  		val = path.Join(val, entity.Name)
    74  		if entity.Self.Type == "Datacenter" {
    75  			d.DatacenterPath = val
    76  		}
    77  	}
    78  
    79  	d.InventoryPath = val
    80  
    81  	return nil
    82  }
    83  
    84  func (d Datastore) Path(path string) string {
    85  	var p DatastorePath
    86  	if p.FromString(path) {
    87  		return p.String() // already in "[datastore] path" format
    88  	}
    89  
    90  	return (&DatastorePath{
    91  		Datastore: d.Name(),
    92  		Path:      path,
    93  	}).String()
    94  }
    95  
    96  // NewDatastoreURL constructs a url.URL with the given file path for datastore access over HTTP.
    97  func NewDatastoreURL(base url.URL, dcPath, dsName, path string) *url.URL {
    98  	scheme := base.Scheme
    99  	// In rare cases where vCenter and ESX are accessed using different schemes.
   100  	if overrideScheme := os.Getenv("GOVMOMI_DATASTORE_ACCESS_SCHEME"); overrideScheme != "" {
   101  		scheme = overrideScheme
   102  	}
   103  
   104  	base.Scheme = scheme
   105  	base.Path = fmt.Sprintf("/folder/%s", path)
   106  	base.RawQuery = url.Values{
   107  		"dcPath": []string{dcPath},
   108  		"dsName": []string{dsName},
   109  	}.Encode()
   110  
   111  	return &base
   112  }
   113  
   114  // NewURL constructs a url.URL with the given file path for datastore access over HTTP.
   115  // The Datastore object is used to derive url, dcPath and dsName params to NewDatastoreURL.
   116  // For dcPath, Datastore.DatacenterPath must be set and for dsName, Datastore.InventoryPath.
   117  // This is the case when the object.Datastore instance is created by Finder.
   118  // Otherwise, Datastore.FindInventoryPath should be called first, to set DatacenterPath
   119  // and InventoryPath.
   120  func (d Datastore) NewURL(path string) *url.URL {
   121  	u := d.c.URL()
   122  	return NewDatastoreURL(*u, d.DatacenterPath, d.Name(), path)
   123  }
   124  
   125  func (d Datastore) Browser(ctx context.Context) (*HostDatastoreBrowser, error) {
   126  	var do mo.Datastore
   127  
   128  	err := d.Properties(ctx, d.Reference(), []string{"browser"}, &do)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	return NewHostDatastoreBrowser(d.c, do.Browser), nil
   134  }
   135  
   136  func (d Datastore) useServiceTicket() bool {
   137  	// If connected to workstation, service ticketing not supported
   138  	// If connected to ESX, service ticketing not needed
   139  	if !d.c.IsVC() {
   140  		return false
   141  	}
   142  
   143  	key := "GOVMOMI_USE_SERVICE_TICKET"
   144  
   145  	val := d.c.URL().Query().Get(key)
   146  	if val == "" {
   147  		val = os.Getenv(key)
   148  	}
   149  
   150  	if val == "1" || val == "true" {
   151  		return true
   152  	}
   153  
   154  	return false
   155  }
   156  
   157  func (d Datastore) useServiceTicketHostName(name string) bool {
   158  	// No need if talking directly to ESX.
   159  	if !d.c.IsVC() {
   160  		return false
   161  	}
   162  
   163  	// If version happens to be < 5.1
   164  	if name == "" {
   165  		return false
   166  	}
   167  
   168  	// If the HostSystem is using DHCP on a network without dynamic DNS,
   169  	// HostSystem.Config.Network.DnsConfig.HostName is set to "localhost" by default.
   170  	// This resolves to "localhost.localdomain" by default via /etc/hosts on ESX.
   171  	// In that case, we will stick with the HostSystem.Name which is the IP address that
   172  	// was used to connect the host to VC.
   173  	if name == "localhost.localdomain" {
   174  		return false
   175  	}
   176  
   177  	// Still possible to have HostName that don't resolve via DNS,
   178  	// so we default to false.
   179  	key := "GOVMOMI_USE_SERVICE_TICKET_HOSTNAME"
   180  
   181  	val := d.c.URL().Query().Get(key)
   182  	if val == "" {
   183  		val = os.Getenv(key)
   184  	}
   185  
   186  	if val == "1" || val == "true" {
   187  		return true
   188  	}
   189  
   190  	return false
   191  }
   192  
   193  type datastoreServiceTicketHostKey struct{}
   194  
   195  // HostContext returns a Context where the given host will be used for datastore HTTP access
   196  // via the ServiceTicket method.
   197  func (d Datastore) HostContext(ctx context.Context, host *HostSystem) context.Context {
   198  	return context.WithValue(ctx, datastoreServiceTicketHostKey{}, host)
   199  }
   200  
   201  // ServiceTicket obtains a ticket via AcquireGenericServiceTicket and returns it an http.Cookie with the url.URL
   202  // that can be used along with the ticket cookie to access the given path.  An host is chosen at random unless the
   203  // the given Context was created with a specific host via the HostContext method.
   204  func (d Datastore) ServiceTicket(ctx context.Context, path string, method string) (*url.URL, *http.Cookie, error) {
   205  	if d.InventoryPath == "" {
   206  		_ = d.FindInventoryPath(ctx)
   207  	}
   208  
   209  	u := d.NewURL(path)
   210  
   211  	host, ok := ctx.Value(datastoreServiceTicketHostKey{}).(*HostSystem)
   212  
   213  	if !ok {
   214  		if !d.useServiceTicket() {
   215  			return u, nil, nil
   216  		}
   217  
   218  		hosts, err := d.AttachedHosts(ctx)
   219  		if err != nil {
   220  			return nil, nil, err
   221  		}
   222  
   223  		if len(hosts) == 0 {
   224  			// Fallback to letting vCenter choose a host
   225  			return u, nil, nil
   226  		}
   227  
   228  		// Pick a random attached host
   229  		host = hosts[rand.Intn(len(hosts))]
   230  	}
   231  
   232  	ips, err := host.ManagementIPs(ctx)
   233  	if err != nil {
   234  		return nil, nil, err
   235  	}
   236  
   237  	if len(ips) > 0 {
   238  		// prefer a ManagementIP
   239  		u.Host = ips[0].String()
   240  	} else {
   241  		// fallback to inventory name
   242  		u.Host, err = host.ObjectName(ctx)
   243  		if err != nil {
   244  			return nil, nil, err
   245  		}
   246  	}
   247  
   248  	// VC datacenter path will not be valid against ESX
   249  	q := u.Query()
   250  	delete(q, "dcPath")
   251  	u.RawQuery = q.Encode()
   252  
   253  	// Now that we have a host selected, take a copy of the URL.
   254  	transferURL := *u
   255  
   256  	if internal.UsingEnvoySidecar(d.Client()) {
   257  		// Rewrite the host URL to go through the Envoy sidecar on VC.
   258  		// Reciever must use a custom dialer.
   259  		u = internal.HostGatewayTransferURL(u, host.Reference())
   260  	}
   261  
   262  	spec := types.SessionManagerHttpServiceRequestSpec{
   263  		// Use the original URL (without rewrites) for the session ticket.
   264  		Url: transferURL.String(),
   265  		// See SessionManagerHttpServiceRequestSpecMethod enum
   266  		Method: fmt.Sprintf("http%s%s", method[0:1], strings.ToLower(method[1:])),
   267  	}
   268  
   269  	sm := session.NewManager(d.Client())
   270  
   271  	ticket, err := sm.AcquireGenericServiceTicket(ctx, &spec)
   272  	if err != nil {
   273  		return nil, nil, err
   274  	}
   275  
   276  	cookie := &http.Cookie{
   277  		Name:  "vmware_cgi_ticket",
   278  		Value: ticket.Id,
   279  	}
   280  
   281  	if d.useServiceTicketHostName(ticket.HostName) {
   282  		u.Host = ticket.HostName
   283  	}
   284  
   285  	d.Client().SetThumbprint(u.Host, ticket.SslThumbprint)
   286  
   287  	return u, cookie, nil
   288  }
   289  
   290  func (d Datastore) uploadTicket(ctx context.Context, path string, param *soap.Upload) (*url.URL, *soap.Upload, error) {
   291  	p := soap.DefaultUpload
   292  	if param != nil {
   293  		p = *param // copy
   294  	}
   295  
   296  	u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
   297  	if err != nil {
   298  		return nil, nil, err
   299  	}
   300  
   301  	if ticket != nil {
   302  		p.Ticket = ticket
   303  		p.Close = true // disable Keep-Alive connection to ESX
   304  	}
   305  
   306  	return u, &p, nil
   307  }
   308  
   309  func (d Datastore) downloadTicket(ctx context.Context, path string, param *soap.Download) (*url.URL, *soap.Download, error) {
   310  	p := soap.DefaultDownload
   311  	if param != nil {
   312  		p = *param // copy
   313  	}
   314  
   315  	u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
   316  	if err != nil {
   317  		return nil, nil, err
   318  	}
   319  
   320  	if ticket != nil {
   321  		p.Ticket = ticket
   322  		p.Close = true // disable Keep-Alive connection to ESX
   323  	}
   324  
   325  	return u, &p, nil
   326  }
   327  
   328  // Upload via soap.Upload with an http service ticket
   329  func (d Datastore) Upload(ctx context.Context, f io.Reader, path string, param *soap.Upload) error {
   330  	u, p, err := d.uploadTicket(ctx, path, param)
   331  	if err != nil {
   332  		return err
   333  	}
   334  	return d.Client().Upload(ctx, f, u, p)
   335  }
   336  
   337  // UploadFile via soap.Upload with an http service ticket
   338  func (d Datastore) UploadFile(ctx context.Context, file string, path string, param *soap.Upload) error {
   339  	u, p, err := d.uploadTicket(ctx, path, param)
   340  	if err != nil {
   341  		return err
   342  	}
   343  	vc := d.Client()
   344  	if internal.UsingEnvoySidecar(vc) {
   345  		// Override the vim client with a new one that wraps a Unix socket transport.
   346  		// Using HTTP here so secure means nothing.
   347  		vc = internal.ClientWithEnvoyHostGateway(vc)
   348  	}
   349  	return vc.UploadFile(ctx, file, u, p)
   350  }
   351  
   352  // Download via soap.Download with an http service ticket
   353  func (d Datastore) Download(ctx context.Context, path string, param *soap.Download) (io.ReadCloser, int64, error) {
   354  	u, p, err := d.downloadTicket(ctx, path, param)
   355  	if err != nil {
   356  		return nil, 0, err
   357  	}
   358  	return d.Client().Download(ctx, u, p)
   359  }
   360  
   361  // DownloadFile via soap.Download with an http service ticket
   362  func (d Datastore) DownloadFile(ctx context.Context, path string, file string, param *soap.Download) error {
   363  	u, p, err := d.downloadTicket(ctx, path, param)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	vc := d.Client()
   368  	if internal.UsingEnvoySidecar(vc) {
   369  		// Override the vim client with a new one that wraps a Unix socket transport.
   370  		// Using HTTP here so secure means nothing.
   371  		vc = internal.ClientWithEnvoyHostGateway(vc)
   372  	}
   373  	return vc.DownloadFile(ctx, file, u, p)
   374  }
   375  
   376  // AttachedHosts returns hosts that have this Datastore attached, accessible and writable.
   377  func (d Datastore) AttachedHosts(ctx context.Context) ([]*HostSystem, error) {
   378  	var ds mo.Datastore
   379  	var hosts []*HostSystem
   380  
   381  	pc := property.DefaultCollector(d.Client())
   382  	err := pc.RetrieveOne(ctx, d.Reference(), []string{"host"}, &ds)
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  
   387  	mounts := make(map[types.ManagedObjectReference]types.DatastoreHostMount)
   388  	var refs []types.ManagedObjectReference
   389  	for _, host := range ds.Host {
   390  		refs = append(refs, host.Key)
   391  		mounts[host.Key] = host
   392  	}
   393  
   394  	var hs []mo.HostSystem
   395  	err = pc.Retrieve(ctx, refs, []string{"runtime.connectionState", "runtime.powerState"}, &hs)
   396  	if err != nil {
   397  		return nil, err
   398  	}
   399  
   400  	for _, host := range hs {
   401  		if host.Runtime.ConnectionState == types.HostSystemConnectionStateConnected &&
   402  			host.Runtime.PowerState == types.HostSystemPowerStatePoweredOn {
   403  
   404  			mount := mounts[host.Reference()]
   405  			info := mount.MountInfo
   406  
   407  			if *info.Mounted && *info.Accessible && info.AccessMode == string(types.HostMountModeReadWrite) {
   408  				hosts = append(hosts, NewHostSystem(d.Client(), mount.Key))
   409  			}
   410  		}
   411  	}
   412  
   413  	return hosts, nil
   414  }
   415  
   416  // AttachedClusterHosts returns hosts that have this Datastore attached, accessible and writable and are members of the given cluster.
   417  func (d Datastore) AttachedClusterHosts(ctx context.Context, cluster *ComputeResource) ([]*HostSystem, error) {
   418  	var hosts []*HostSystem
   419  
   420  	clusterHosts, err := cluster.Hosts(ctx)
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  
   425  	attachedHosts, err := d.AttachedHosts(ctx)
   426  	if err != nil {
   427  		return nil, err
   428  	}
   429  
   430  	refs := make(map[types.ManagedObjectReference]bool)
   431  	for _, host := range attachedHosts {
   432  		refs[host.Reference()] = true
   433  	}
   434  
   435  	for _, host := range clusterHosts {
   436  		if refs[host.Reference()] {
   437  			hosts = append(hosts, host)
   438  		}
   439  	}
   440  
   441  	return hosts, nil
   442  }
   443  
   444  func (d Datastore) Stat(ctx context.Context, file string) (types.BaseFileInfo, error) {
   445  	b, err := d.Browser(ctx)
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	spec := types.HostDatastoreBrowserSearchSpec{
   451  		Details: &types.FileQueryFlags{
   452  			FileType:     true,
   453  			FileSize:     true,
   454  			Modification: true,
   455  			FileOwner:    types.NewBool(true),
   456  		},
   457  		MatchPattern: []string{path.Base(file)},
   458  	}
   459  
   460  	dsPath := d.Path(path.Dir(file))
   461  	task, err := b.SearchDatastore(ctx, dsPath, &spec)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  
   466  	info, err := task.WaitForResult(ctx, nil)
   467  	if err != nil {
   468  		if types.IsFileNotFound(err) {
   469  			// FileNotFound means the base path doesn't exist.
   470  			return nil, DatastoreNoSuchDirectoryError{"stat", dsPath}
   471  		}
   472  
   473  		return nil, err
   474  	}
   475  
   476  	res := info.Result.(types.HostDatastoreBrowserSearchResults)
   477  	if len(res.File) == 0 {
   478  		// File doesn't exist
   479  		return nil, DatastoreNoSuchFileError{"stat", d.Path(file)}
   480  	}
   481  
   482  	return res.File[0], nil
   483  
   484  }
   485  
   486  // Type returns the type of file system volume.
   487  func (d Datastore) Type(ctx context.Context) (types.HostFileSystemVolumeFileSystemType, error) {
   488  	var mds mo.Datastore
   489  
   490  	if err := d.Properties(ctx, d.Reference(), []string{"summary.type"}, &mds); err != nil {
   491  		return types.HostFileSystemVolumeFileSystemType(""), err
   492  	}
   493  	return types.HostFileSystemVolumeFileSystemType(mds.Summary.Type), nil
   494  }