Repository: vmware/govmomi
Pull request #1763 (2019-12-20 20:36:07)

Added the missing RetrieveSnapshotDetails API in VSLM
The RetrieveSnapshotDetails API is necessary for the incremental backup of FCD or CNS block volume. Somehow, the API is not added initially.


Patch:
diff --git a/simulator/simulator.go b/simulator/simulator.go
index 944fa987..ae58106e 100644
--- a/simulator/simulator.go
+++ b/simulator/simulator.go
@@ -50,11 +50,16 @@ import (
 	"github.com/vmware/govmomi/vim25/xml"
 )
 
-// Trace when set to true, writes SOAP traffic to stderr
-var Trace = false
+var (
+	// Trace when set to true, writes SOAP traffic to stderr
+	Trace = false
+
+	// TraceFile is the output file when Trace = true
+	TraceFile = os.Stderr
 
-// DefaultLogin for authentication
-var DefaultLogin = url.UserPassword("user", "pass")
+	// DefaultLogin for authentication
+	DefaultLogin = url.UserPassword("user", "pass")
+)
 
 // Method encapsulates a decoded SOAP client request
 type Method struct {
@@ -412,6 +417,11 @@ func (s *Service) RegisterSDK(r *Registry) {
 	s.ServeMux.HandleFunc(r.Path, s.ServeSDK)
 }
 
+// StatusSDK can be used to simulate an /sdk HTTP response code other than 200.
+// The value of StatusSDK is restored to http.StatusOK after 1 response.
+// This can be useful to test vim25.Retry() for example.
+var StatusSDK = http.StatusOK
+
 // ServeSDK implements the http.Handler interface
 func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
@@ -419,6 +429,12 @@ func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if StatusSDK != http.StatusOK {
+		w.WriteHeader(StatusSDK)
+		StatusSDK = http.StatusOK // reset
+		return
+	}
+
 	body, err := s.readAll(r.Body)
 	_ = r.Body.Close()
 	if err != nil {
@@ -428,7 +444,7 @@ func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if Trace {
-		fmt.Fprintf(os.Stderr, "Request: %s\n", string(body))
+		fmt.Fprintf(TraceFile, "Request: %s\n", string(body))
 	}
 
 	ctx := &Context{
@@ -499,7 +515,7 @@ func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if Trace {
-		fmt.Fprintf(os.Stderr, "Response: %s\n", out.String())
+		fmt.Fprintf(TraceFile, "Response: %s\n", out.String())
 	}
 
 	_, _ = w.Write(out.Bytes())
diff --git a/vcsim/main.go b/vcsim/main.go
index b571fafd..246bda0a 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -72,7 +72,8 @@ func main() {
 	user := flag.String("username", "", "Login username for vcsim (any username allowed by default)")
 	pass := flag.String("password", "", "Login password for vcsim (any password allowed by default)")
 	tunnel := flag.Int("tunnel", -1, "SDK tunnel port")
-	flag.BoolVar(&simulator.Trace, "trace", simulator.Trace, "Trace SOAP to stderr")
+	flag.BoolVar(&simulator.Trace, "trace", simulator.Trace, "Trace SOAP to -trace-file")
+	trace := flag.String("trace-file", "", "Trace output file (defaults to stderr)")
 	stdinExit := flag.Bool("stdinexit", false, "Press any key to exit")
 	dir := flag.String("load", "", "Load model from directory")
 
@@ -81,6 +82,16 @@ func main() {
 	flag.Float64Var(&model.DelayConfig.DelayJitter, "delay-jitter", model.DelayConfig.DelayJitter, "Delay jitter coefficient of variation (tip: 0.5 is a good starting value)")
 
 	flag.Parse()
+
+	if *trace != "" {
+		var err error
+		simulator.TraceFile, err = os.Create(*trace)
+		if err != nil {
+			log.Fatal(err)
+		}
+		simulator.Trace = true
+	}
+
 	methodDelay := *methodDelayP
 	u := &url.URL{Host: *listen}
 	if *user != "" {
@@ -211,6 +222,10 @@ func main() {
 	<-sig
 
 	model.Remove()
+
+	if *trace != "" {
+		_ = simulator.TraceFile.Close()
+	}
 }
 
 func updateHostTemplate(ip string) error {
diff --git a/vim25/retry.go b/vim25/retry.go
index b8807eef..f10e2cb6 100644
--- a/vim25/retry.go
+++ b/vim25/retry.go
@@ -18,8 +18,6 @@ package vim25
 
 import (
 	"context"
-	"net"
-	"net/url"
 	"time"
 
 	"github.com/vmware/govmomi/vim25/soap"
@@ -32,22 +30,18 @@ type RetryFunc func(err error) (retry bool, delay time.Duration)
 // network error (for example: a connect timeout).
 func TemporaryNetworkError(n int) RetryFunc {
 	return func(err error) (retry bool, delay time.Duration) {
-		var nerr net.Error
 		var ok bool
 
-		// Never retry if this is not a network error.
-		switch rerr := err.(type) {
-		case *url.Error:
-			if nerr, ok = rerr.Err.(net.Error); !ok {
-				return false, 0
-			}
-		case net.Error:
-			nerr = rerr
-		default:
+		t, ok := err.(interface {
+			// Temporary is implemented by url.Error and net.Error
+			Temporary() bool
+		})
+		if !ok {
+			// Never retry if this is not a Temporary error.
 			return false, 0
 		}
 
-		if !nerr.Temporary() {
+		if !t.Temporary() {
 			return false, 0
 		}
 
diff --git a/vim25/soap/client.go b/vim25/soap/client.go
index 5b725f33..e6acd3d0 100644
--- a/vim25/soap/client.go
+++ b/vim25/soap/client.go
@@ -532,6 +532,32 @@ func (c *Client) WithHeader(ctx context.Context, header Header) context.Context
 	return context.WithValue(ctx, headerContext{}, header)
 }
 
+type statusError struct {
+	res *http.Response
+}
+
+// Temporary returns true for HTTP response codes that can be retried
+// See vim25.TemporaryNetworkError
+func (e *statusError) Temporary() bool {
+	switch e.res.StatusCode {
+	case http.StatusBadGateway:
+		return true
+	}
+	return false
+}
+
+func (e *statusError) Error() string {
+	return e.res.Status
+}
+
+func newStatusError(res *http.Response) error {
+	return &url.Error{
+		Op:  res.Request.Method,
+		URL: res.Request.URL.Path,
+		Err: &statusError{res},
+	}
+}
+
 func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error {
 	var err error
 	var b []byte
@@ -587,7 +613,7 @@ func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error
 		case http.StatusInternalServerError:
 			// Error, but typically includes a body explaining the error
 		default:
-			return errors.New(res.Status)
+			return newStatusError(res)
 		}
 
 		dec := xml.NewDecoder(res.Body)
diff --git a/vslm/global_object_manager.go b/vslm/global_object_manager.go
index 60cb208f..29c8f4f3 100644
--- a/vslm/global_object_manager.go
+++ b/vslm/global_object_manager.go
@@ -530,6 +530,22 @@ func (this *GlobalObjectManager) Revert(ctx context.Context, id vim.ID, snapshot
 	return NewTask(this.c, res.Returnval), nil
 }
 
+func (this *GlobalObjectManager) RetrieveSnapshotDetails(ctx context.Context, id vim.ID, snapshotId vim.ID) (
+	*vim.VStorageObjectSnapshotDetails, error) {
+	req := types.VslmRetrieveSnapshotDetails{
+		This:       this.Reference(),
+		Id:         id,
+		SnapshotId: snapshotId,
+	}
+
+	res, err := methods.VslmRetrieveSnapshotDetails(ctx, this.c, &req)
+	if err != nil {
+		return nil, err
+	}
+
+	return &res.Returnval, nil
+}
+
 func (this *GlobalObjectManager) QueryChangedDiskAreas(ctx context.Context, id vim.ID, snapshotId vim.ID, startOffset int64,
 	changeId string) (*vim.DiskChangeInfo, error) {
 	req := types.VslmQueryChangedDiskAreas{


Test patch:
diff --git a/govc/test/vcsim.bats b/govc/test/vcsim.bats
index 7a848bbf..c9010bf9 100755
--- a/govc/test/vcsim.bats
+++ b/govc/test/vcsim.bats
@@ -418,3 +418,19 @@ docker_name() {
   objs=$(govc find / | wc -l)
   assert_equal 23 "$objs"
 }
+
+@test "vcsim trace file" {
+  file="$BATS_TMPDIR/$(new_id).trace"
+
+  vcsim_start -trace-file "$file"
+
+  run govc ls
+  assert_success
+
+  vcsim_stop
+
+  run ls -l "$file"
+  assert_success
+
+  rm "$file"
+}
diff --git a/vim25/examples_test.go b/vim25/examples_test.go
new file mode 100644
index 00000000..f4bd6999
--- /dev/null
+++ b/vim25/examples_test.go
@@ -0,0 +1,52 @@
+/*
+Copyright (c) 2019 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 vim25_test
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vim25"
+)
+
+func ExampleTemporaryNetworkError() {
+	simulator.Run(func(ctx context.Context, c *vim25.Client) error {
+		// Configure retry handler
+		c.RoundTripper = vim25.Retry(c.Client, vim25.TemporaryNetworkError(3))
+
+		vm, err := find.NewFinder(c).VirtualMachine(ctx, "DC0_H0_VM0")
+		if err != nil {
+			return err
+		}
+
+		// Tell vcsim to respond with 502 on the 1st request
+		simulator.StatusSDK = http.StatusBadGateway
+
+		state, err := vm.PowerState(ctx)
+		if err != nil {
+			return err
+		}
+
+		fmt.Println(state)
+
+		return nil
+	})
+	// Output: poweredOn
+}