436 lines
11 KiB
Go
436 lines
11 KiB
Go
|
/*
|
||
|
Copyright (c) 2015-2016 VMware, Inc. All Rights Reserved.
|
||
|
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package object
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math/rand"
|
||
|
"os"
|
||
|
"path"
|
||
|
"strings"
|
||
|
|
||
|
"context"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
|
||
|
"github.com/vmware/govmomi/property"
|
||
|
"github.com/vmware/govmomi/session"
|
||
|
"github.com/vmware/govmomi/vim25"
|
||
|
"github.com/vmware/govmomi/vim25/mo"
|
||
|
"github.com/vmware/govmomi/vim25/soap"
|
||
|
"github.com/vmware/govmomi/vim25/types"
|
||
|
)
|
||
|
|
||
|
// DatastoreNoSuchDirectoryError is returned when a directory could not be found.
|
||
|
type DatastoreNoSuchDirectoryError struct {
|
||
|
verb string
|
||
|
subject string
|
||
|
}
|
||
|
|
||
|
func (e DatastoreNoSuchDirectoryError) Error() string {
|
||
|
return fmt.Sprintf("cannot %s '%s': No such directory", e.verb, e.subject)
|
||
|
}
|
||
|
|
||
|
// DatastoreNoSuchFileError is returned when a file could not be found.
|
||
|
type DatastoreNoSuchFileError struct {
|
||
|
verb string
|
||
|
subject string
|
||
|
}
|
||
|
|
||
|
func (e DatastoreNoSuchFileError) Error() string {
|
||
|
return fmt.Sprintf("cannot %s '%s': No such file", e.verb, e.subject)
|
||
|
}
|
||
|
|
||
|
type Datastore struct {
|
||
|
Common
|
||
|
|
||
|
DatacenterPath string
|
||
|
}
|
||
|
|
||
|
func NewDatastore(c *vim25.Client, ref types.ManagedObjectReference) *Datastore {
|
||
|
return &Datastore{
|
||
|
Common: NewCommon(c, ref),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d Datastore) Path(path string) string {
|
||
|
return (&DatastorePath{
|
||
|
Datastore: d.Name(),
|
||
|
Path: path,
|
||
|
}).String()
|
||
|
}
|
||
|
|
||
|
// NewURL constructs a url.URL with the given file path for datastore access over HTTP.
|
||
|
func (d Datastore) NewURL(path string) *url.URL {
|
||
|
u := d.c.URL()
|
||
|
|
||
|
return &url.URL{
|
||
|
Scheme: u.Scheme,
|
||
|
Host: u.Host,
|
||
|
Path: fmt.Sprintf("/folder/%s", path),
|
||
|
RawQuery: url.Values{
|
||
|
"dcPath": []string{d.DatacenterPath},
|
||
|
"dsName": []string{d.Name()},
|
||
|
}.Encode(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// URL is deprecated, use NewURL instead.
|
||
|
func (d Datastore) URL(ctx context.Context, dc *Datacenter, path string) (*url.URL, error) {
|
||
|
return d.NewURL(path), nil
|
||
|
}
|
||
|
|
||
|
func (d Datastore) Browser(ctx context.Context) (*HostDatastoreBrowser, error) {
|
||
|
var do mo.Datastore
|
||
|
|
||
|
err := d.Properties(ctx, d.Reference(), []string{"browser"}, &do)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return NewHostDatastoreBrowser(d.c, do.Browser), nil
|
||
|
}
|
||
|
|
||
|
func (d Datastore) useServiceTicket() bool {
|
||
|
// If connected to workstation, service ticketing not supported
|
||
|
// If connected to ESX, service ticketing not needed
|
||
|
if !d.c.IsVC() {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
key := "GOVMOMI_USE_SERVICE_TICKET"
|
||
|
|
||
|
val := d.c.URL().Query().Get(key)
|
||
|
if val == "" {
|
||
|
val = os.Getenv(key)
|
||
|
}
|
||
|
|
||
|
if val == "1" || val == "true" {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (d Datastore) useServiceTicketHostName(name string) bool {
|
||
|
// No need if talking directly to ESX.
|
||
|
if !d.c.IsVC() {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// If version happens to be < 5.1
|
||
|
if name == "" {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// If the HostSystem is using DHCP on a network without dynamic DNS,
|
||
|
// HostSystem.Config.Network.DnsConfig.HostName is set to "localhost" by default.
|
||
|
// This resolves to "localhost.localdomain" by default via /etc/hosts on ESX.
|
||
|
// In that case, we will stick with the HostSystem.Name which is the IP address that
|
||
|
// was used to connect the host to VC.
|
||
|
if name == "localhost.localdomain" {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Still possible to have HostName that don't resolve via DNS,
|
||
|
// so we default to false.
|
||
|
key := "GOVMOMI_USE_SERVICE_TICKET_HOSTNAME"
|
||
|
|
||
|
val := d.c.URL().Query().Get(key)
|
||
|
if val == "" {
|
||
|
val = os.Getenv(key)
|
||
|
}
|
||
|
|
||
|
if val == "1" || val == "true" {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
type datastoreServiceTicketHostKey struct{}
|
||
|
|
||
|
// HostContext returns a Context where the given host will be used for datastore HTTP access
|
||
|
// via the ServiceTicket method.
|
||
|
func (d Datastore) HostContext(ctx context.Context, host *HostSystem) context.Context {
|
||
|
return context.WithValue(ctx, datastoreServiceTicketHostKey{}, host)
|
||
|
}
|
||
|
|
||
|
// ServiceTicket obtains a ticket via AcquireGenericServiceTicket and returns it an http.Cookie with the url.URL
|
||
|
// that can be used along with the ticket cookie to access the given path. An host is chosen at random unless the
|
||
|
// the given Context was created with a specific host via the HostContext method.
|
||
|
func (d Datastore) ServiceTicket(ctx context.Context, path string, method string) (*url.URL, *http.Cookie, error) {
|
||
|
u := d.NewURL(path)
|
||
|
|
||
|
host, ok := ctx.Value(datastoreServiceTicketHostKey{}).(*HostSystem)
|
||
|
|
||
|
if !ok {
|
||
|
if !d.useServiceTicket() {
|
||
|
return u, nil, nil
|
||
|
}
|
||
|
|
||
|
hosts, err := d.AttachedHosts(ctx)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
if len(hosts) == 0 {
|
||
|
// Fallback to letting vCenter choose a host
|
||
|
return u, nil, nil
|
||
|
}
|
||
|
|
||
|
// Pick a random attached host
|
||
|
host = hosts[rand.Intn(len(hosts))]
|
||
|
}
|
||
|
|
||
|
ips, err := host.ManagementIPs(ctx)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
if len(ips) > 0 {
|
||
|
// prefer a ManagementIP
|
||
|
u.Host = ips[0].String()
|
||
|
} else {
|
||
|
// fallback to inventory name
|
||
|
u.Host, err = host.ObjectName(ctx)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// VC datacenter path will not be valid against ESX
|
||
|
q := u.Query()
|
||
|
delete(q, "dcPath")
|
||
|
u.RawQuery = q.Encode()
|
||
|
|
||
|
spec := types.SessionManagerHttpServiceRequestSpec{
|
||
|
Url: u.String(),
|
||
|
// See SessionManagerHttpServiceRequestSpecMethod enum
|
||
|
Method: fmt.Sprintf("http%s%s", method[0:1], strings.ToLower(method[1:])),
|
||
|
}
|
||
|
|
||
|
sm := session.NewManager(d.Client())
|
||
|
|
||
|
ticket, err := sm.AcquireGenericServiceTicket(ctx, &spec)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
cookie := &http.Cookie{
|
||
|
Name: "vmware_cgi_ticket",
|
||
|
Value: ticket.Id,
|
||
|
}
|
||
|
|
||
|
if d.useServiceTicketHostName(ticket.HostName) {
|
||
|
u.Host = ticket.HostName
|
||
|
}
|
||
|
|
||
|
d.Client().SetThumbprint(u.Host, ticket.SslThumbprint)
|
||
|
|
||
|
return u, cookie, nil
|
||
|
}
|
||
|
|
||
|
func (d Datastore) uploadTicket(ctx context.Context, path string, param *soap.Upload) (*url.URL, *soap.Upload, error) {
|
||
|
p := soap.DefaultUpload
|
||
|
if param != nil {
|
||
|
p = *param // copy
|
||
|
}
|
||
|
|
||
|
u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
p.Ticket = ticket
|
||
|
|
||
|
return u, &p, nil
|
||
|
}
|
||
|
|
||
|
func (d Datastore) downloadTicket(ctx context.Context, path string, param *soap.Download) (*url.URL, *soap.Download, error) {
|
||
|
p := soap.DefaultDownload
|
||
|
if param != nil {
|
||
|
p = *param // copy
|
||
|
}
|
||
|
|
||
|
u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
p.Ticket = ticket
|
||
|
|
||
|
return u, &p, nil
|
||
|
}
|
||
|
|
||
|
// Upload via soap.Upload with an http service ticket
|
||
|
func (d Datastore) Upload(ctx context.Context, f io.Reader, path string, param *soap.Upload) error {
|
||
|
u, p, err := d.uploadTicket(ctx, path, param)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return d.Client().Upload(ctx, f, u, p)
|
||
|
}
|
||
|
|
||
|
// UploadFile via soap.Upload with an http service ticket
|
||
|
func (d Datastore) UploadFile(ctx context.Context, file string, path string, param *soap.Upload) error {
|
||
|
u, p, err := d.uploadTicket(ctx, path, param)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return d.Client().UploadFile(ctx, file, u, p)
|
||
|
}
|
||
|
|
||
|
// Download via soap.Download with an http service ticket
|
||
|
func (d Datastore) Download(ctx context.Context, path string, param *soap.Download) (io.ReadCloser, int64, error) {
|
||
|
u, p, err := d.downloadTicket(ctx, path, param)
|
||
|
if err != nil {
|
||
|
return nil, 0, err
|
||
|
}
|
||
|
return d.Client().Download(ctx, u, p)
|
||
|
}
|
||
|
|
||
|
// DownloadFile via soap.Download with an http service ticket
|
||
|
func (d Datastore) DownloadFile(ctx context.Context, path string, file string, param *soap.Download) error {
|
||
|
u, p, err := d.downloadTicket(ctx, path, param)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return d.Client().DownloadFile(ctx, file, u, p)
|
||
|
}
|
||
|
|
||
|
// AttachedHosts returns hosts that have this Datastore attached, accessible and writable.
|
||
|
func (d Datastore) AttachedHosts(ctx context.Context) ([]*HostSystem, error) {
|
||
|
var ds mo.Datastore
|
||
|
var hosts []*HostSystem
|
||
|
|
||
|
pc := property.DefaultCollector(d.Client())
|
||
|
err := pc.RetrieveOne(ctx, d.Reference(), []string{"host"}, &ds)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
mounts := make(map[types.ManagedObjectReference]types.DatastoreHostMount)
|
||
|
var refs []types.ManagedObjectReference
|
||
|
for _, host := range ds.Host {
|
||
|
refs = append(refs, host.Key)
|
||
|
mounts[host.Key] = host
|
||
|
}
|
||
|
|
||
|
var hs []mo.HostSystem
|
||
|
err = pc.Retrieve(ctx, refs, []string{"runtime.connectionState", "runtime.powerState"}, &hs)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for _, host := range hs {
|
||
|
if host.Runtime.ConnectionState == types.HostSystemConnectionStateConnected &&
|
||
|
host.Runtime.PowerState == types.HostSystemPowerStatePoweredOn {
|
||
|
|
||
|
mount := mounts[host.Reference()]
|
||
|
info := mount.MountInfo
|
||
|
|
||
|
if *info.Mounted && *info.Accessible && info.AccessMode == string(types.HostMountModeReadWrite) {
|
||
|
hosts = append(hosts, NewHostSystem(d.Client(), mount.Key))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return hosts, nil
|
||
|
}
|
||
|
|
||
|
// AttachedClusterHosts returns hosts that have this Datastore attached, accessible and writable and are members of the given cluster.
|
||
|
func (d Datastore) AttachedClusterHosts(ctx context.Context, cluster *ComputeResource) ([]*HostSystem, error) {
|
||
|
var hosts []*HostSystem
|
||
|
|
||
|
clusterHosts, err := cluster.Hosts(ctx)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
attachedHosts, err := d.AttachedHosts(ctx)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
refs := make(map[types.ManagedObjectReference]bool)
|
||
|
for _, host := range attachedHosts {
|
||
|
refs[host.Reference()] = true
|
||
|
}
|
||
|
|
||
|
for _, host := range clusterHosts {
|
||
|
if refs[host.Reference()] {
|
||
|
hosts = append(hosts, host)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return hosts, nil
|
||
|
}
|
||
|
|
||
|
func (d Datastore) Stat(ctx context.Context, file string) (types.BaseFileInfo, error) {
|
||
|
b, err := d.Browser(ctx)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
spec := types.HostDatastoreBrowserSearchSpec{
|
||
|
Details: &types.FileQueryFlags{
|
||
|
FileType: true,
|
||
|
FileSize: true,
|
||
|
Modification: true,
|
||
|
FileOwner: types.NewBool(true),
|
||
|
},
|
||
|
MatchPattern: []string{path.Base(file)},
|
||
|
}
|
||
|
|
||
|
dsPath := d.Path(path.Dir(file))
|
||
|
task, err := b.SearchDatastore(ctx, dsPath, &spec)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
info, err := task.WaitForResult(ctx, nil)
|
||
|
if err != nil {
|
||
|
if types.IsFileNotFound(err) {
|
||
|
// FileNotFound means the base path doesn't exist.
|
||
|
return nil, DatastoreNoSuchDirectoryError{"stat", dsPath}
|
||
|
}
|
||
|
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
res := info.Result.(types.HostDatastoreBrowserSearchResults)
|
||
|
if len(res.File) == 0 {
|
||
|
// File doesn't exist
|
||
|
return nil, DatastoreNoSuchFileError{"stat", d.Path(file)}
|
||
|
}
|
||
|
|
||
|
return res.File[0], nil
|
||
|
|
||
|
}
|
||
|
|
||
|
// Type returns the type of file system volume.
|
||
|
func (d Datastore) Type(ctx context.Context) (types.HostFileSystemVolumeFileSystemType, error) {
|
||
|
var mds mo.Datastore
|
||
|
|
||
|
if err := d.Properties(ctx, d.Reference(), []string{"summary.type"}, &mds); err != nil {
|
||
|
return types.HostFileSystemVolumeFileSystemType(""), err
|
||
|
}
|
||
|
return types.HostFileSystemVolumeFileSystemType(mds.Summary.Type), nil
|
||
|
}
|