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 }