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