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

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