Repository: vmware/govmomi (go)

Issue:
Support CSV format response in QueryPerf API
**Is your feature request related to a problem? Please describe.**
QueryPerf API PerfQuerySpec argument allows clients to specify the return format for performance counter data ("normal", "csv"); using CSV format is recommended by vmware for performance reasons, but govcsim does not support the format property at this time. Clients that request performance counter data in CSV format will not receive data in the format they expect.

https://vdc-repo.vmware.com/vmwb-repository/dcr-public/c476b64b-c93c-4b21-9d76-be14da0148f9/04ca12ad-59b9-4e1c-8232-fd3d4276e52c/SDK/vsphere-ws/docs/ReferenceGuide/vim.PerformanceManager.QuerySpec.html

https://vdc-repo.vmware.com/vmwb-repository/dcr-public/c476b64b-c93c-4b21-9d76-be14da0148f9/04ca12ad-59b9-4e1c-8232-fd3d4276e52c/SDK/vsphere-ws/docs/ReferenceGuide/vim.PerformanceManager.Format.html

https://vdc-repo.vmware.com/vmwb-repository/dcr-public/c476b64b-c93c-4b21-9d76-be14da0148f9/04ca12ad-59b9-4e1c-8232-fd3d4276e52c/SDK/vsphere-ws/docs/ReferenceGuide/vim.PerformanceManager.EntityMetricCSV.html

**Describe the solution you'd like**
When format is specified as "csv" on the PerfQuerySpec argument to QueryPerf, return performance counter data in CSV format, otherwise return data in the "normal" format - the behaviour of govcsim today.

**Describe alternatives you've considered**
Behaviour is defined by the vmware API; this is just a small gap in govcsim implementation at this time.

Pull request description:
govcsim: Add CSV format support to QueryPerf API; Closes #3103
## Description

Adds "csv" format support to the QueryPerf API implementation in govcsim; performance counter data is generated the same as before, but if the client specifies "csv" format, samples are converted to CSV before being returned, otherwise the "normal" format is returned (previous behaviour).

Closes: #3103

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to
  not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. If applicable, please also list any relevant
details for your test configuration.

- [ ] vmware client tool that requests performance counters in CSV format
- [ ] Unit tests

## Checklist:

- [x] My code follows the `CONTRIBUTION`
  [guidelines](https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md) of
  this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged

Patch:
diff --git a/simulator/performance_manager.go b/simulator/performance_manager.go
index 71f18cb7..1319b3c4 100644
--- a/simulator/performance_manager.go
+++ b/simulator/performance_manager.go
@@ -19,6 +19,7 @@ package simulator
 import (
 	"math/rand"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/vmware/govmomi/simulator/esx"
@@ -174,9 +175,6 @@ func (p *PerformanceManager) QueryPerf(ctx *Context, req *types.QueryPerf) soap.
 	body.Res.Returnval = make([]types.BasePerfEntityMetricBase, len(req.QuerySpec))
 
 	for i, qs := range req.QuerySpec {
-		metrics := new(types.PerfEntityMetric)
-		metrics.Entity = qs.Entity
-
 		// Get metric data for this entity type
 		metricData, ok := p.metricData[qs.Entity.Type]
 		if !ok {
@@ -206,6 +204,9 @@ func (p *PerformanceManager) QueryPerf(ctx *Context, req *types.QueryPerf) soap.
 			n = qs.MaxSample
 		}
 
+		metrics := new(types.PerfEntityMetric)
+		metrics.Entity = qs.Entity
+
 		// Loop through each interval "tick"
 		metrics.SampleInfo = make([]types.PerfSampleInfo, n)
 		metrics.Value = make([]types.BasePerfMetricSeries, len(qs.MetricId))
@@ -213,10 +214,11 @@ func (p *PerformanceManager) QueryPerf(ctx *Context, req *types.QueryPerf) soap.
 			metrics.SampleInfo[tick] = types.PerfSampleInfo{Timestamp: end.Add(time.Duration(-interval*tick) * time.Second), Interval: interval}
 		}
 
+		series := make([]*types.PerfMetricIntSeries, len(qs.MetricId))
 		for j, mid := range qs.MetricId {
 			// Create list of metrics for this tick
-			series := &types.PerfMetricIntSeries{Value: make([]int64, n)}
-			series.Id = mid
+			series[j] = &types.PerfMetricIntSeries{Value: make([]int64, n)}
+			series[j].Id = mid
 			points := metricData[mid.CounterId]
 			offset := int64(start.Unix()) / int64(interval)
 
@@ -237,11 +239,56 @@ func (p *PerformanceManager) QueryPerf(ctx *Context, req *types.QueryPerf) soap.
 				} else {
 					p = 0
 				}
-				series.Value[tick] = p
+				series[j].Value[tick] = p
 			}
-			metrics.Value[j] = series
+			metrics.Value[j] = series[j]
+		}
+
+		if qs.Format == string(types.PerfFormatCsv) {
+			metricsCsv := new(types.PerfEntityMetricCSV)
+			metricsCsv.Entity = qs.Entity
+
+			//PerfSampleInfo encoded in the following CSV format: [interval1], [date1], [interval2], [date2], and so on.
+			metricsCsv.SampleInfoCSV = sampleInfoCSV(metrics)
+			metricsCsv.Value = make([]types.PerfMetricSeriesCSV, len(qs.MetricId))
+
+			for j, mid := range qs.MetricId {
+				seriesCsv := &types.PerfMetricSeriesCSV{Value: ""}
+				seriesCsv.Id = mid
+				seriesCsv.Value = valueCSV(series[j])
+				metricsCsv.Value[j] = *seriesCsv
+			}
+
+			body.Res.Returnval[i] = metricsCsv
+		} else {
+			body.Res.Returnval[i] = metrics
 		}
-		body.Res.Returnval[i] = metrics
 	}
 	return body
 }
+
+// sampleInfoCSV converts the SampleInfo field to a CSV string
+func sampleInfoCSV(m *types.PerfEntityMetric) string {
+	values := make([]string, len(m.SampleInfo)*2)
+
+	i := 0
+	for _, s := range m.SampleInfo {
+		values[i] = strconv.Itoa(int(s.Interval))
+		i++
+		values[i] = s.Timestamp.Format(time.RFC3339)
+		i++
+	}
+
+	return strings.Join(values, ",")
+}
+
+// valueCSV converts the Value field to a CSV string
+func valueCSV(s *types.PerfMetricIntSeries) string {
+	values := make([]string, len(s.Value))
+
+	for i := range s.Value {
+		values[i] = strconv.FormatInt(s.Value[i], 10)
+	}
+
+	return strings.Join(values, ",")
+}


Test patch:
diff --git a/simulator/performance_manager_test.go b/simulator/performance_manager_test.go
index e8f45360..8c9cac7d 100644
--- a/simulator/performance_manager_test.go
+++ b/simulator/performance_manager_test.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/vmware/govmomi/performance"
@@ -306,6 +307,53 @@ func testPerfQuery(ctx context.Context, m *Model, e mo.Entity, interval int32, m
 	return nil
 }
 
+func testPerfQueryCSV(ctx context.Context, m *Model, e mo.Entity, interval int32, maxSample int32) error {
+	c := m.Service.client
+
+	p := performance.NewManager(c)
+
+	// Single metric, single VM
+	//
+	qs := []types.PerfQuerySpec{
+		{
+			MaxSample:  maxSample,
+			IntervalId: interval,
+			MetricId:   []types.PerfMetricId{{CounterId: 1, Instance: ""}},
+			Entity:     e.Reference(),
+			Format:     string(types.PerfFormatCsv),
+		},
+	}
+	series, err := p.Query(ctx, qs)
+	if err != nil {
+		return err
+	}
+	if len(series) == 0 {
+		return errors.New("Empty result set")
+	}
+	for i := range series {
+		s, ok := series[i].(*types.PerfEntityMetricCSV)
+		if !ok {
+			panic(fmt.Errorf("expected type %T, got: %T", s, series[i]))
+		}
+		if len(s.SampleInfoCSV) == 0 {
+			return errors.New("Empty SampleInfoCSV")
+		}
+		if len(strings.Split(s.SampleInfoCSV, ",")) == 0 {
+			return errors.New("SampleInfoCSV not in CSV format")
+		}
+		for _, v := range s.Value {
+			if len(v.Value) == 0 {
+				return errors.New("Empty PerfEntityMetricCSV.Value")
+			}
+			if len(strings.Split(v.Value, ",")) == 0 {
+				return errors.New("PerfEntityMetricCSV.Value not in CSV format")
+			}
+		}
+	}
+
+	return nil
+}
+
 func TestQueryPerf(t *testing.T) {
 	ctx := context.Background()
 
@@ -337,5 +385,25 @@ func TestQueryPerf(t *testing.T) {
 		if err := testPerfQuery(ctx, m, Map.Any("ResourcePool"), 300, maxSample); err != nil {
 			t.Fatal(err)
 		}
+
+		//csv format
+		if err := testPerfQueryCSV(ctx, m, Map.Any("VirtualMachine"), 20, maxSample); err != nil {
+			t.Fatal(err)
+		}
+		if err := testPerfQueryCSV(ctx, m, Map.Any("HostSystem"), 20, maxSample); err != nil {
+			t.Fatal(err)
+		}
+		if err := testPerfQueryCSV(ctx, m, Map.Any("ClusterComputeResource"), 300, maxSample); err != nil {
+			t.Fatal(err)
+		}
+		if err := testPerfQueryCSV(ctx, m, Map.Any("Datastore"), 300, maxSample); err != nil {
+			t.Fatal(err)
+		}
+		if err := testPerfQueryCSV(ctx, m, Map.Any("Datacenter"), 300, maxSample); err != nil {
+			t.Fatal(err)
+		}
+		if err := testPerfQueryCSV(ctx, m, Map.Any("ResourcePool"), 300, maxSample); err != nil {
+			t.Fatal(err)
+		}
 	}
 }