datastore.go 11 KB

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