github.com/vmware/govmomi@v0.43.0/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 }