datastore.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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/internal"
  25. "github.com/vmware/govmomi/property"
  26. "github.com/vmware/govmomi/session"
  27. "github.com/vmware/govmomi/vim25"
  28. "github.com/vmware/govmomi/vim25/mo"
  29. "github.com/vmware/govmomi/vim25/soap"
  30. "github.com/vmware/govmomi/vim25/types"
  31. )
  32. // DatastoreNoSuchDirectoryError is returned when a directory could not be found.
  33. type DatastoreNoSuchDirectoryError struct {
  34. verb string
  35. subject string
  36. }
  37. func (e DatastoreNoSuchDirectoryError) Error() string {
  38. return fmt.Sprintf("cannot %s '%s': No such directory", e.verb, e.subject)
  39. }
  40. // DatastoreNoSuchFileError is returned when a file could not be found.
  41. type DatastoreNoSuchFileError struct {
  42. verb string
  43. subject string
  44. }
  45. func (e DatastoreNoSuchFileError) Error() string {
  46. return fmt.Sprintf("cannot %s '%s': No such file", e.verb, e.subject)
  47. }
  48. type Datastore struct {
  49. Common
  50. DatacenterPath string
  51. }
  52. func NewDatastore(c *vim25.Client, ref types.ManagedObjectReference) *Datastore {
  53. return &Datastore{
  54. Common: NewCommon(c, ref),
  55. }
  56. }
  57. // FindInventoryPath sets InventoryPath and DatacenterPath,
  58. // needed by NewURL() to compose an upload/download endpoint URL
  59. func (d *Datastore) FindInventoryPath(ctx context.Context) error {
  60. entities, err := mo.Ancestors(ctx, d.c, d.c.ServiceContent.PropertyCollector, d.r)
  61. if err != nil {
  62. return err
  63. }
  64. val := "/"
  65. for _, entity := range entities {
  66. if entity.Parent == nil {
  67. continue // root folder
  68. }
  69. val = path.Join(val, entity.Name)
  70. if entity.Self.Type == "Datacenter" {
  71. d.DatacenterPath = val
  72. }
  73. }
  74. d.InventoryPath = val
  75. return nil
  76. }
  77. func (d Datastore) Path(path string) string {
  78. var p DatastorePath
  79. if p.FromString(path) {
  80. return p.String() // already in "[datastore] path" format
  81. }
  82. return (&DatastorePath{
  83. Datastore: d.Name(),
  84. Path: path,
  85. }).String()
  86. }
  87. // NewURL constructs a url.URL with the given file path for datastore access over HTTP.
  88. func (d Datastore) NewURL(path string) *url.URL {
  89. u := d.c.URL()
  90. scheme := u.Scheme
  91. // In rare cases where vCenter and ESX are accessed using different schemes.
  92. if overrideScheme := os.Getenv("GOVMOMI_DATASTORE_ACCESS_SCHEME"); overrideScheme != "" {
  93. scheme = overrideScheme
  94. }
  95. return &url.URL{
  96. Scheme: scheme,
  97. Host: u.Host,
  98. Path: fmt.Sprintf("/folder/%s", path),
  99. RawQuery: url.Values{
  100. "dcPath": []string{d.DatacenterPath},
  101. "dsName": []string{d.Name()},
  102. }.Encode(),
  103. }
  104. }
  105. func (d Datastore) Browser(ctx context.Context) (*HostDatastoreBrowser, error) {
  106. var do mo.Datastore
  107. err := d.Properties(ctx, d.Reference(), []string{"browser"}, &do)
  108. if err != nil {
  109. return nil, err
  110. }
  111. return NewHostDatastoreBrowser(d.c, do.Browser), nil
  112. }
  113. func (d Datastore) useServiceTicket() bool {
  114. // If connected to workstation, service ticketing not supported
  115. // If connected to ESX, service ticketing not needed
  116. if !d.c.IsVC() {
  117. return false
  118. }
  119. key := "GOVMOMI_USE_SERVICE_TICKET"
  120. val := d.c.URL().Query().Get(key)
  121. if val == "" {
  122. val = os.Getenv(key)
  123. }
  124. if val == "1" || val == "true" {
  125. return true
  126. }
  127. return false
  128. }
  129. func (d Datastore) useServiceTicketHostName(name string) bool {
  130. // No need if talking directly to ESX.
  131. if !d.c.IsVC() {
  132. return false
  133. }
  134. // If version happens to be < 5.1
  135. if name == "" {
  136. return false
  137. }
  138. // If the HostSystem is using DHCP on a network without dynamic DNS,
  139. // HostSystem.Config.Network.DnsConfig.HostName is set to "localhost" by default.
  140. // This resolves to "localhost.localdomain" by default via /etc/hosts on ESX.
  141. // In that case, we will stick with the HostSystem.Name which is the IP address that
  142. // was used to connect the host to VC.
  143. if name == "localhost.localdomain" {
  144. return false
  145. }
  146. // Still possible to have HostName that don't resolve via DNS,
  147. // so we default to false.
  148. key := "GOVMOMI_USE_SERVICE_TICKET_HOSTNAME"
  149. val := d.c.URL().Query().Get(key)
  150. if val == "" {
  151. val = os.Getenv(key)
  152. }
  153. if val == "1" || val == "true" {
  154. return true
  155. }
  156. return false
  157. }
  158. type datastoreServiceTicketHostKey struct{}
  159. // HostContext returns a Context where the given host will be used for datastore HTTP access
  160. // via the ServiceTicket method.
  161. func (d Datastore) HostContext(ctx context.Context, host *HostSystem) context.Context {
  162. return context.WithValue(ctx, datastoreServiceTicketHostKey{}, host)
  163. }
  164. // ServiceTicket obtains a ticket via AcquireGenericServiceTicket and returns it an http.Cookie with the url.URL
  165. // that can be used along with the ticket cookie to access the given path. An host is chosen at random unless the
  166. // the given Context was created with a specific host via the HostContext method.
  167. func (d Datastore) ServiceTicket(ctx context.Context, path string, method string) (*url.URL, *http.Cookie, error) {
  168. if d.InventoryPath == "" {
  169. _ = d.FindInventoryPath(ctx)
  170. }
  171. u := d.NewURL(path)
  172. host, ok := ctx.Value(datastoreServiceTicketHostKey{}).(*HostSystem)
  173. if !ok {
  174. if !d.useServiceTicket() {
  175. return u, nil, nil
  176. }
  177. hosts, err := d.AttachedHosts(ctx)
  178. if err != nil {
  179. return nil, nil, err
  180. }
  181. if len(hosts) == 0 {
  182. // Fallback to letting vCenter choose a host
  183. return u, nil, nil
  184. }
  185. // Pick a random attached host
  186. host = hosts[rand.Intn(len(hosts))]
  187. }
  188. ips, err := host.ManagementIPs(ctx)
  189. if err != nil {
  190. return nil, nil, err
  191. }
  192. if len(ips) > 0 {
  193. // prefer a ManagementIP
  194. u.Host = ips[0].String()
  195. } else {
  196. // fallback to inventory name
  197. u.Host, err = host.ObjectName(ctx)
  198. if err != nil {
  199. return nil, nil, err
  200. }
  201. }
  202. // VC datacenter path will not be valid against ESX
  203. q := u.Query()
  204. delete(q, "dcPath")
  205. u.RawQuery = q.Encode()
  206. // Now that we have a host selected, take a copy of the URL.
  207. transferURL := *u
  208. if internal.UsingEnvoySidecar(d.Client()) {
  209. // Rewrite the host URL to go through the Envoy sidecar on VC.
  210. // Reciever must use a custom dialer.
  211. u = internal.HostGatewayTransferURL(u, host.Reference())
  212. }
  213. spec := types.SessionManagerHttpServiceRequestSpec{
  214. // Use the original URL (without rewrites) for the session ticket.
  215. Url: transferURL.String(),
  216. // See SessionManagerHttpServiceRequestSpecMethod enum
  217. Method: fmt.Sprintf("http%s%s", method[0:1], strings.ToLower(method[1:])),
  218. }
  219. sm := session.NewManager(d.Client())
  220. ticket, err := sm.AcquireGenericServiceTicket(ctx, &spec)
  221. if err != nil {
  222. return nil, nil, err
  223. }
  224. cookie := &http.Cookie{
  225. Name: "vmware_cgi_ticket",
  226. Value: ticket.Id,
  227. }
  228. if d.useServiceTicketHostName(ticket.HostName) {
  229. u.Host = ticket.HostName
  230. }
  231. d.Client().SetThumbprint(u.Host, ticket.SslThumbprint)
  232. return u, cookie, nil
  233. }
  234. func (d Datastore) uploadTicket(ctx context.Context, path string, param *soap.Upload) (*url.URL, *soap.Upload, error) {
  235. p := soap.DefaultUpload
  236. if param != nil {
  237. p = *param // copy
  238. }
  239. u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
  240. if err != nil {
  241. return nil, nil, err
  242. }
  243. if ticket != nil {
  244. p.Ticket = ticket
  245. p.Close = true // disable Keep-Alive connection to ESX
  246. }
  247. return u, &p, nil
  248. }
  249. func (d Datastore) downloadTicket(ctx context.Context, path string, param *soap.Download) (*url.URL, *soap.Download, error) {
  250. p := soap.DefaultDownload
  251. if param != nil {
  252. p = *param // copy
  253. }
  254. u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
  255. if err != nil {
  256. return nil, nil, err
  257. }
  258. if ticket != nil {
  259. p.Ticket = ticket
  260. p.Close = true // disable Keep-Alive connection to ESX
  261. }
  262. return u, &p, nil
  263. }
  264. // Upload via soap.Upload with an http service ticket
  265. func (d Datastore) Upload(ctx context.Context, f io.Reader, path string, param *soap.Upload) error {
  266. u, p, err := d.uploadTicket(ctx, path, param)
  267. if err != nil {
  268. return err
  269. }
  270. return d.Client().Upload(ctx, f, u, p)
  271. }
  272. // UploadFile via soap.Upload with an http service ticket
  273. func (d Datastore) UploadFile(ctx context.Context, file string, path string, param *soap.Upload) error {
  274. u, p, err := d.uploadTicket(ctx, path, param)
  275. if err != nil {
  276. return err
  277. }
  278. vc := d.Client()
  279. if internal.UsingEnvoySidecar(vc) {
  280. // Override the vim client with a new one that wraps a Unix socket transport.
  281. // Using HTTP here so secure means nothing.
  282. vc = internal.ClientWithEnvoyHostGateway(vc)
  283. }
  284. return vc.UploadFile(ctx, file, u, p)
  285. }
  286. // Download via soap.Download with an http service ticket
  287. func (d Datastore) Download(ctx context.Context, path string, param *soap.Download) (io.ReadCloser, int64, error) {
  288. u, p, err := d.downloadTicket(ctx, path, param)
  289. if err != nil {
  290. return nil, 0, err
  291. }
  292. return d.Client().Download(ctx, u, p)
  293. }
  294. // DownloadFile via soap.Download with an http service ticket
  295. func (d Datastore) DownloadFile(ctx context.Context, path string, file string, param *soap.Download) error {
  296. u, p, err := d.downloadTicket(ctx, path, param)
  297. if err != nil {
  298. return err
  299. }
  300. vc := d.Client()
  301. if internal.UsingEnvoySidecar(vc) {
  302. // Override the vim client with a new one that wraps a Unix socket transport.
  303. // Using HTTP here so secure means nothing.
  304. vc = internal.ClientWithEnvoyHostGateway(vc)
  305. }
  306. return vc.DownloadFile(ctx, file, u, p)
  307. }
  308. // AttachedHosts returns hosts that have this Datastore attached, accessible and writable.
  309. func (d Datastore) AttachedHosts(ctx context.Context) ([]*HostSystem, error) {
  310. var ds mo.Datastore
  311. var hosts []*HostSystem
  312. pc := property.DefaultCollector(d.Client())
  313. err := pc.RetrieveOne(ctx, d.Reference(), []string{"host"}, &ds)
  314. if err != nil {
  315. return nil, err
  316. }
  317. mounts := make(map[types.ManagedObjectReference]types.DatastoreHostMount)
  318. var refs []types.ManagedObjectReference
  319. for _, host := range ds.Host {
  320. refs = append(refs, host.Key)
  321. mounts[host.Key] = host
  322. }
  323. var hs []mo.HostSystem
  324. err = pc.Retrieve(ctx, refs, []string{"runtime.connectionState", "runtime.powerState"}, &hs)
  325. if err != nil {
  326. return nil, err
  327. }
  328. for _, host := range hs {
  329. if host.Runtime.ConnectionState == types.HostSystemConnectionStateConnected &&
  330. host.Runtime.PowerState == types.HostSystemPowerStatePoweredOn {
  331. mount := mounts[host.Reference()]
  332. info := mount.MountInfo
  333. if *info.Mounted && *info.Accessible && info.AccessMode == string(types.HostMountModeReadWrite) {
  334. hosts = append(hosts, NewHostSystem(d.Client(), mount.Key))
  335. }
  336. }
  337. }
  338. return hosts, nil
  339. }
  340. // AttachedClusterHosts returns hosts that have this Datastore attached, accessible and writable and are members of the given cluster.
  341. func (d Datastore) AttachedClusterHosts(ctx context.Context, cluster *ComputeResource) ([]*HostSystem, error) {
  342. var hosts []*HostSystem
  343. clusterHosts, err := cluster.Hosts(ctx)
  344. if err != nil {
  345. return nil, err
  346. }
  347. attachedHosts, err := d.AttachedHosts(ctx)
  348. if err != nil {
  349. return nil, err
  350. }
  351. refs := make(map[types.ManagedObjectReference]bool)
  352. for _, host := range attachedHosts {
  353. refs[host.Reference()] = true
  354. }
  355. for _, host := range clusterHosts {
  356. if refs[host.Reference()] {
  357. hosts = append(hosts, host)
  358. }
  359. }
  360. return hosts, nil
  361. }
  362. func (d Datastore) Stat(ctx context.Context, file string) (types.BaseFileInfo, error) {
  363. b, err := d.Browser(ctx)
  364. if err != nil {
  365. return nil, err
  366. }
  367. spec := types.HostDatastoreBrowserSearchSpec{
  368. Details: &types.FileQueryFlags{
  369. FileType: true,
  370. FileSize: true,
  371. Modification: true,
  372. FileOwner: types.NewBool(true),
  373. },
  374. MatchPattern: []string{path.Base(file)},
  375. }
  376. dsPath := d.Path(path.Dir(file))
  377. task, err := b.SearchDatastore(ctx, dsPath, &spec)
  378. if err != nil {
  379. return nil, err
  380. }
  381. info, err := task.WaitForResult(ctx, nil)
  382. if err != nil {
  383. if types.IsFileNotFound(err) {
  384. // FileNotFound means the base path doesn't exist.
  385. return nil, DatastoreNoSuchDirectoryError{"stat", dsPath}
  386. }
  387. return nil, err
  388. }
  389. res := info.Result.(types.HostDatastoreBrowserSearchResults)
  390. if len(res.File) == 0 {
  391. // File doesn't exist
  392. return nil, DatastoreNoSuchFileError{"stat", d.Path(file)}
  393. }
  394. return res.File[0], nil
  395. }
  396. // Type returns the type of file system volume.
  397. func (d Datastore) Type(ctx context.Context) (types.HostFileSystemVolumeFileSystemType, error) {
  398. var mds mo.Datastore
  399. if err := d.Properties(ctx, d.Reference(), []string{"summary.type"}, &mds); err != nil {
  400. return types.HostFileSystemVolumeFileSystemType(""), err
  401. }
  402. return types.HostFileSystemVolumeFileSystemType(mds.Summary.Type), nil
  403. }