blob: 5ab84af6b0e5f9f0788122f5273a21e9f58cf830 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 main
import (
"csbench/domain"
"csbench/network"
"csbench/vm"
"csbench/volume"
"flag"
"fmt"
"io"
"math"
"os"
"strings"
"time"
"csbench/apirunner"
"csbench/config"
log "github.com/sirupsen/logrus"
"github.com/apache/cloudstack-go/v2/cloudstack"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/montanaflynn/stats"
"github.com/sourcegraph/conc/pool"
)
var (
profiles = make(map[int]*config.Profile)
)
type Result struct {
Success bool
Duration float64
}
func init() {
logFile, err := os.OpenFile("csmetrics.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Failed to create log file: %v", err)
}
mw := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(mw)
}
func readConfigurations(configFile string) map[int]*config.Profile {
profiles, err := config.ReadProfiles(configFile)
if err != nil {
log.Fatal("Error reading profiles:", err)
}
return profiles
}
func logConfigurationDetails(profiles map[int]*config.Profile) {
apiURL := config.URL
iterations := config.Iterations
page := config.Page
pagesize := config.PageSize
host := config.Host
userProfileNames := make([]string, 0, len(profiles))
for _, profile := range profiles {
userProfileNames = append(userProfileNames, profile.Name)
}
fmt.Printf("\n\n\033[1;34mBenchmarking the CloudStack environment [%s] with the following configuration\033[0m\n\n", apiURL)
fmt.Printf("Management server : %s\n", host)
fmt.Printf("Roles : %s\n", strings.Join(userProfileNames, ","))
fmt.Printf("Iterations : %d\n", iterations)
fmt.Printf("Page : %d\n", page)
fmt.Printf("PageSize : %d\n\n", pagesize)
log.Infof("Found %d profiles in the configuration: ", len(profiles))
log.Infof("Management server : %s", host)
}
func logReport() {
fmt.Printf("\n\n\nLog file : csmetrics.log\n")
fmt.Printf("Reports directory per API : report/%s/\n", config.Host)
fmt.Printf("Number of APIs : %d\n", apirunner.APIscount)
fmt.Printf("Successful APIs : %d\n", apirunner.SuccessAPIs)
fmt.Printf("Failed APIs : %d\n", apirunner.FailedAPIs)
fmt.Printf("Time in seconds per API: %.2f (avg)\n", apirunner.TotalTime/float64(apirunner.APIscount))
fmt.Printf("\n\n\033[1;34m--------------------------------------------------------------------------------\033[0m\n" +
" Done with benchmarking\n" +
"\033[1;34m--------------------------------------------------------------------------------\033[0m\n\n")
}
func getSamples(results []*Result) (stats.Float64Data, stats.Float64Data, stats.Float64Data) {
var allExecutionsSample stats.Float64Data
var successfulExecutionSample stats.Float64Data
var failedExecutionSample stats.Float64Data
for _, result := range results {
duration := math.Round(result.Duration*1000) / 1000
allExecutionsSample = append(allExecutionsSample, duration)
if result.Success {
successfulExecutionSample = append(successfulExecutionSample, duration)
} else {
failedExecutionSample = append(failedExecutionSample, duration)
}
}
return allExecutionsSample, successfulExecutionSample, failedExecutionSample
}
func getRowFromSample(key string, sample stats.Float64Data) table.Row {
min, _ := sample.Min()
min = math.Round(min*1000) / 1000
max, _ := sample.Max()
max = math.Round(max*1000) / 1000
mean, _ := sample.Mean()
mean = math.Round(mean*1000) / 1000
median, _ := sample.Median()
median = math.Round(median*1000) / 1000
percentile90, _ := sample.Percentile(90)
percentile90 = math.Round(percentile90*1000) / 1000
percentile95, _ := sample.Percentile(95)
percentile95 = math.Round(percentile95*1000) / 1000
percentile99, _ := sample.Percentile(99)
percentile99 = math.Round(percentile99*1000) / 1000
return table.Row{key, len(sample), min, max, mean, median, percentile90, percentile95, percentile99}
}
/*
This function will generate a report with the following details:
1. Total Number of executions
2. Number of successful executions
3. Number of failed exections
4. Different statistics like min, max, avg, median, 90th percentile, 95th percentile, 99th percentile for above 3
Output format:
1. CSV
2. TSV
3. Table
*/
func generateReport(results map[string][]*Result, format string, outputFile string) {
fmt.Println("Generating report")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Type", "Count", "Min", "Max", "Avg", "Median", "90th percentile", "95th percentile", "99th percentile"})
for key, result := range results {
allExecutionsSample, successfulExecutionSample, failedExecutionSample := getSamples(result)
t.AppendRow(getRowFromSample(fmt.Sprintf("%s - All", key), allExecutionsSample))
if failedExecutionSample.Len() != 0 {
t.AppendRow(getRowFromSample(fmt.Sprintf("%s - Successful", key), successfulExecutionSample))
t.AppendRow(getRowFromSample(fmt.Sprintf("%s - Failed", key), failedExecutionSample))
}
}
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
log.Error("Error creating file: ", err)
}
defer f.Close()
t.SetOutputMirror(f)
}
switch format {
case "csv":
t.RenderCSV()
case "tsv":
t.RenderTSV()
case "table":
t.Render()
}
}
func main() {
dbprofile := flag.Int("dbprofile", 0, "DB profile number")
create := flag.Bool("create", false, "Create resources")
benchmark := flag.Bool("benchmark", false, "Benchmark list APIs")
domainFlag := flag.Bool("domain", false, "Create domain")
limitsFlag := flag.Bool("limits", false, "Update limits to -1")
networkFlag := flag.Bool("network", false, "Create shared network")
vmFlag := flag.Bool("vm", false, "Deploy VMs")
volumeFlag := flag.Bool("volume", false, "Attach Volumes to VMs")
tearDown := flag.Bool("teardown", false, "Tear down all subdomains")
workers := flag.Int("workers", 10, "number of workers to use while creating resources")
format := flag.String("format", "table", "Format of the report (csv, tsv, table). Valid only for create")
outputFile := flag.String("output", "", "Path to output file. Valid only for create")
configFile := flag.String("config", "config/config", "Path to config file")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: go run csmetrictool.go -dbprofile <DB profile number>\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}
flag.Parse()
if !(*create || *benchmark || *tearDown) {
log.Fatal("Please provide one of the following options: -create, -benchmark, -teardown")
}
if *create && !(*domainFlag || *limitsFlag || *networkFlag || *vmFlag || *volumeFlag) {
log.Fatal("Please provide one of the following options with create: -domain, -limits, -network, -vm, -volume")
}
switch *format {
case "csv", "tsv", "table":
// valid format, continue
default:
log.Fatal("Invalid format. Please provide one of the following: csv, tsv, table")
}
if *dbprofile < 0 {
log.Fatal("Invalid DB profile number. Please provide a positive integer.")
}
profiles = readConfigurations(*configFile)
apiURL := config.URL
iterations := config.Iterations
page := config.Page
pagesize := config.PageSize
if *create {
results := createResources(domainFlag, limitsFlag, networkFlag, vmFlag, volumeFlag, workers)
generateReport(results, *format, *outputFile)
}
if *benchmark {
log.Infof("\nStarted benchmarking the CloudStack environment [%s]", apiURL)
logConfigurationDetails(profiles)
for i, profile := range profiles {
userProfileName := profile.Name
log.Infof("Using profile %d.%s for benchmarking", i, userProfileName)
fmt.Printf("\n\033[1;34m============================================================\033[0m\n")
fmt.Printf(" Profile: [%s]\n", userProfileName)
fmt.Printf("\033[1;34m============================================================\033[0m\n")
apirunner.RunAPIs(userProfileName, apiURL, profile.ApiKey, profile.SecretKey, profile.Expires, profile.SignatureVersion, iterations, page, pagesize, *dbprofile)
}
logReport()
log.Infof("Done with benchmarking the CloudStack environment [%s]", apiURL)
}
if *tearDown {
tearDownEnv()
}
}
func createResources(domainFlag, limitsFlag, networkFlag, vmFlag, volumeFlag *bool, workers *int) map[string][]*Result {
apiURL := config.URL
for _, profile := range profiles {
if profile.Name == "admin" {
numVmsPerNetwork := config.NumVms
numVolumesPerVM := config.NumVolumes
cs := cloudstack.NewAsyncClient(apiURL, profile.ApiKey, profile.SecretKey, false)
var results = make(map[string][]*Result)
if *domainFlag {
workerPool := pool.NewWithResults[*Result]().WithMaxGoroutines(*workers)
results["domain"] = createDomains(workerPool, cs, config.ParentDomainId, config.NumDomains)
}
if *limitsFlag {
workerPool := pool.NewWithResults[*Result]().WithMaxGoroutines(*workers)
results["limits"] = updateLimits(workerPool, cs, config.ParentDomainId)
}
if *networkFlag {
workerPool := pool.NewWithResults[*Result]().WithMaxGoroutines(*workers)
results["network"] = createNetwork(workerPool, cs, config.ParentDomainId)
}
if *vmFlag {
workerPool := pool.NewWithResults[*Result]().WithMaxGoroutines(*workers)
results["vm"] = createVms(workerPool, cs, config.ParentDomainId, numVmsPerNetwork)
}
if *volumeFlag {
workerPool := pool.NewWithResults[*Result]().WithMaxGoroutines(*workers)
results["volume"] = createVolumes(workerPool, cs, config.ParentDomainId, numVolumesPerVM)
}
return results
}
}
return nil
}
func createDomains(workerPool *pool.ResultPool[*Result], cs *cloudstack.CloudStackClient, parentDomainId string, count int) []*Result {
progressMarker := int(math.Ceil(float64(count) / 10.0))
start := time.Now()
log.Infof("Creating %d domains", count)
for i := 0; i < count; i++ {
if (i+1)%progressMarker == 0 {
log.Infof("Created %d domains", i+1)
}
workerPool.Go(func() *Result {
taskStart := time.Now()
dmn, err := domain.CreateDomain(cs, parentDomainId)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
_, err = domain.CreateAccount(cs, dmn.Id)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
return &Result{
Success: true,
Duration: time.Since(taskStart).Seconds(),
}
})
}
res := workerPool.Wait()
log.Infof("Created %d domains in %.2f seconds", count, time.Since(start).Seconds())
return res
}
func updateLimits(workerPool *pool.ResultPool[*Result], cs *cloudstack.CloudStackClient, parentDomainId string) []*Result {
log.Infof("Fetching subdomains for domain %s", parentDomainId)
domains := domain.ListSubDomains(cs, parentDomainId)
accounts := make([]*cloudstack.Account, 0)
for _, dmn := range domains {
accounts = append(accounts, domain.ListAccounts(cs, dmn.Id)...)
}
progressMarker := int(math.Ceil(float64(len(accounts)) / 10.0))
start := time.Now()
log.Infof("Updating limits for %d accounts", len(accounts))
for i, account := range accounts {
if (i+1)%progressMarker == 0 {
log.Infof("Updated limits for %d accounts", i+1)
}
account := account
workerPool.Go(func() *Result {
taskStart := time.Now()
resp := domain.UpdateLimits(cs, account)
return &Result{
Success: resp,
Duration: time.Since(taskStart).Seconds(),
}
})
}
res := workerPool.Wait()
log.Infof("Updated limits for %d accounts in %.2f seconds", len(accounts), time.Since(start).Seconds())
return res
}
func createNetwork(workerPool *pool.ResultPool[*Result], cs *cloudstack.CloudStackClient, parentDomainId string) []*Result {
log.Infof("Fetching subdomains for domain %s", parentDomainId)
domains := domain.ListSubDomains(cs, parentDomainId)
progressMarker := int(math.Ceil(float64(len(domains)) / 10.0))
start := time.Now()
log.Infof("Creating %d networks", len(domains))
for i, dmn := range domains {
if (i+1)%progressMarker == 0 {
log.Infof("Created %d networks", i+1)
}
i := i
dmn := dmn
workerPool.Go(func() *Result {
taskStart := time.Now()
_, err := network.CreateNetwork(cs, dmn.Id, i)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
return &Result{
Success: true,
Duration: time.Since(taskStart).Seconds(),
}
})
}
res := workerPool.Wait()
log.Infof("Created %d networks in %.2f seconds", len(domains), time.Since(start).Seconds())
return res
}
func createVms(workerPool *pool.ResultPool[*Result], cs *cloudstack.CloudStackClient, parentDomainId string, numVmPerNetwork int) []*Result {
log.Infof("Fetching subdomains & accounts for domain %s", parentDomainId)
domains := domain.ListSubDomains(cs, parentDomainId)
var accounts []*cloudstack.Account
for i := 0; i < len(domains); i++ {
account := domain.ListAccounts(cs, domains[i].Id)
accounts = append(accounts, account...)
}
domainIdAccountMapping := make(map[string]*cloudstack.Account)
for _, account := range accounts {
domainIdAccountMapping[account.Domainid] = account
}
log.Infof("Fetching networks for subdomains in domain %s", parentDomainId)
var allNetworks []*cloudstack.Network
for _, domain := range domains {
network, _ := network.ListNetworks(cs, domain.Id)
allNetworks = append(allNetworks, network...)
}
progressMarker := int(math.Ceil(float64(len(allNetworks)*numVmPerNetwork) / 10.0))
start := time.Now()
log.Infof("Creating %d VMs", len(allNetworks)*numVmPerNetwork)
for i, network := range allNetworks {
network := network
for j := 1; j <= numVmPerNetwork; j++ {
if (i*j+j)%progressMarker == 0 {
log.Infof("Created %d VMs", i*j+j)
}
workerPool.Go(func() *Result {
taskStart := time.Now()
_, err := vm.DeployVm(cs, network.Domainid, network.Id, domainIdAccountMapping[network.Domainid].Name)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
return &Result{
Success: true,
Duration: time.Since(taskStart).Seconds(),
}
})
}
}
res := workerPool.Wait()
log.Infof("Created %d VMs in %.2f seconds", len(allNetworks)*numVmPerNetwork, time.Since(start).Seconds())
return res
}
func createVolumes(workerPool *pool.ResultPool[*Result], cs *cloudstack.CloudStackClient, parentDomainId string, numVolumesPerVM int) []*Result {
log.Infof("Fetching all VMs in subdomains for domain %s", parentDomainId)
domains := domain.ListSubDomains(cs, parentDomainId)
var allVMs []*cloudstack.VirtualMachine
for _, dmn := range domains {
vms, err := vm.ListVMs(cs, dmn.Id)
if err != nil {
log.Warn("Error listing VMs: ", err)
continue
}
allVMs = append(allVMs, vms...)
}
progressMarker := int(math.Ceil(float64(len(allVMs)*numVolumesPerVM) / 10.0))
start := time.Now()
log.Infof("Creating %d volumes", len(allVMs)*numVolumesPerVM)
unsuitableVmCount := 0
for i, vm := range allVMs {
vm := vm
if vm.State != "Running" && vm.State != "Stopped" {
unsuitableVmCount++
continue
}
for j := 1; j <= numVolumesPerVM; j++ {
if (i*j+j)%progressMarker == 0 {
log.Infof("Created %d volumes", i*j+j)
}
workerPool.Go(func() *Result {
taskStart := time.Now()
vol, err := volume.CreateVolume(cs, vm.Domainid, vm.Account)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
_, err = volume.AttachVolume(cs, vol.Id, vm.Id)
if err != nil {
return &Result{
Success: false,
Duration: time.Since(taskStart).Seconds(),
}
}
return &Result{
Success: true,
Duration: time.Since(taskStart).Seconds(),
}
})
}
}
if unsuitableVmCount > 0 {
log.Warnf("Found %d VMs in unsuitable state", unsuitableVmCount)
}
res := workerPool.Wait()
log.Infof("Created %d volumes in %.2f seconds", (len(allVMs)-unsuitableVmCount)*numVolumesPerVM, time.Since(start).Seconds())
return res
}
func tearDownEnv() {
parentDomain := config.ParentDomainId
apiURL := config.URL
for _, profile := range profiles {
userProfileName := profile.Name
if userProfileName == "admin" {
cs := cloudstack.NewAsyncClient(apiURL, profile.ApiKey, profile.SecretKey, false)
domains := domain.ListSubDomains(cs, parentDomain)
log.Infof("Deleting %d domains", len(domains))
for _, subdomain := range domains {
domain.DeleteDomain(cs, subdomain.Id)
}
break
}
}
}