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