diff --git a/cmd/billing.go b/cmd/billing.go index 5c9ac8fe..5802f979 100644 --- a/cmd/billing.go +++ b/cmd/billing.go @@ -2,6 +2,8 @@ package cmd import ( "fmt" + "strconv" + "strings" "time" "github.com/fi-ts/cloud-go/api/client/accounting" @@ -12,6 +14,8 @@ import ( "github.com/jinzhu/now" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/xuri/excelize/v2" ) type BillingOpts struct { @@ -24,6 +28,9 @@ type BillingOpts struct { ClusterID string Device string Namespace string + Month string + Year string + Filename string CSV bool } @@ -37,6 +44,21 @@ func newBillingCmd(c *config) *cobra.Command { Short: "manage bills", Long: "TODO", } + excelBillingCmd := &cobra.Command{ + Use: "excel", + Short: "create excel file with monthly billing information for all ressources", + Example: ` + cloudctl billing excel + `, + RunE: func(cmd *cobra.Command, args []string) error { + err := initBillingOpts() + if err != nil { + return err + } + return c.excel() + }, + PreRun: bindPFlags, + } containerBillingCmd := &cobra.Command{ Use: "container", Short: "look at container bills", @@ -162,6 +184,7 @@ func newBillingCmd(c *config) *cobra.Command { PreRun: bindPFlags, } + billingCmd.AddCommand(excelBillingCmd) billingCmd.AddCommand(containerBillingCmd) billingCmd.AddCommand(clusterBillingCmd) billingCmd.AddCommand(ipBillingCmd) @@ -172,6 +195,14 @@ func newBillingCmd(c *config) *cobra.Command { billingOpts = &BillingOpts{} + excelBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") + excelBillingCmd.Flags().StringVarP(&billingOpts.ProjectID, "project-id", "p", "", "the project to account") + excelBillingCmd.Flags().StringVarP(&billingOpts.Month, "month", "m", "", "requested month") + excelBillingCmd.Flags().StringVarP(&billingOpts.Year, "year", "y", "", "requested year") + excelBillingCmd.Flags().StringVarP(&billingOpts.Filename, "file", "f", "", "excel filename") + + must(viper.BindPFlags(excelBillingCmd.Flags())) + containerBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") containerBillingCmd.Flags().StringP("time-format", "", "2006-01-02", "the time format used to parse the arguments 'from' and 'to'") containerBillingCmd.Flags().StringVarP(&billingOpts.FromString, "from", "", "", "the start time in the accounting window to look at (optional, defaults to start of the month") @@ -322,6 +353,447 @@ func (c *config) clusterUsageCSV(cur *models.V1ClusterUsageRequest) error { return nil } +func (c *config) excel() error { + month := int(time.Now().Month()) + year := int(time.Now().Year()) + + var err error + if billingOpts.Month != "" { + if month, err = strconv.Atoi(billingOpts.Month); err != nil { + return err + } + } + if billingOpts.Year != "" { + if year, err = strconv.Atoi(billingOpts.Year); err != nil { + return err + } + } + if billingOpts.Year == "" && int(time.Now().Month()) < month { + year-- + } + + from := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + fromDT := strfmt.DateTime(from) + to := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0) + + f := excelize.NewFile() + + f.SetSheetName("Sheet1", "Parameter") + + clusterSheetName := fmt.Sprintf("%04d-%02d", year, month) + " Cluster" + f.NewSheet(clusterSheetName) + containerSheetName := fmt.Sprintf("%04d-%02d", year, month) + " Container" + f.NewSheet(containerSheetName) + volumeSheetName := fmt.Sprintf("%04d-%02d", year, month) + " Volume" + f.NewSheet(volumeSheetName) + ipSheetName := fmt.Sprintf("%04d-%02d", year, month) + " IPs" + f.NewSheet(ipSheetName) + networkSheetName := fmt.Sprintf("%04d-%02d", year, month) + " Network traffic" + f.NewSheet(networkSheetName) + s3SheetName := fmt.Sprintf("%04d-%02d", year, month) + " S3" + f.NewSheet(s3SheetName) + postgresSheetName := fmt.Sprintf("%04d-%02d", year, month) + " Postgres" + f.NewSheet(postgresSheetName) + + // Parameter + must(f.SetCellValue("Parameter", "A1", "Finance Cloud Native billing")) + if billingOpts.Tenant != "" { + must(f.SetCellValue("Parameter", "A3", "Tenant")) + must(f.SetCellValue("Parameter", "B3", billingOpts.Tenant)) + } + if billingOpts.ProjectID != "" { + must(f.SetCellValue("Parameter", "C3", "ProjectID")) + must(f.SetCellValue("Parameter", "D3", billingOpts.ProjectID)) + } + must(f.SetCellValue("Parameter", "A4", "Period Start")) + must(f.SetCellValue("Parameter", "B4", from)) + must(f.SetCellValue("Parameter", "A5", "Period End")) + if time.Now().Before(to) { + must(f.SetCellValue("Parameter", "B5", time.Now())) + } else { + must(f.SetCellValue("Parameter", "B5", to)) + } + must(f.SetCellValue("Parameter", "A7", "CPU included")) + must(f.SetCellValue("Parameter", "B7", 64)) + must(f.SetCellValue("Parameter", "A8", "RAM included")) + must(f.SetCellValue("Parameter", "B8", 128)) + must(f.SetCellValue("Parameter", "A9", "Local vol included")) + must(f.SetCellValue("Parameter", "B9", 512)) + must(f.SetColWidth("Parameter", "A", "B", 14)) + must(f.SetCellValue("Parameter", "A11", "* Lifetime[s] is always calculated for the given period")) + + // Cluster Billing + curCluster := models.V1ClusterUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curCluster.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curCluster.Projectid = billingOpts.ProjectID + } + + requestCluster := accounting.NewClusterUsageParams() + requestCluster.SetBody(&curCluster) + + responseCluster, err := c.cloud.Accounting.ClusterUsage(requestCluster, nil) + if err != nil { + return err + } + + must(f.SetCellValue(clusterSheetName, "A1", "Tenant")) + must(f.SetCellValue(clusterSheetName, "B1", "Project ID")) + must(f.SetColWidth(clusterSheetName, "B", "B", 0)) + must(f.SetCellValue(clusterSheetName, "C1", "Project")) + must(f.SetCellValue(clusterSheetName, "D1", "Cluster ID")) + must(f.SetColWidth(clusterSheetName, "D", "D", 0)) + must(f.SetCellValue(clusterSheetName, "E1", "Cluster")) + must(f.SetCellValue(clusterSheetName, "F1", "Start")) + must(f.SetColWidth(clusterSheetName, "F", "F", 15)) + must(f.SetCellValue(clusterSheetName, "G1", "Lifetime[s]")) + must(f.SetCellValue(clusterSheetName, "H1", "CPUs used [CPU*h]")) + must(f.SetCellValue(clusterSheetName, "I1", "CPUs on top [CPU*h]")) + must(f.SetCellValue(clusterSheetName, "J1", "CPUs avg [CPU]")) + must(f.SetCellValue(clusterSheetName, "K1", "Memory used [GiB*h]")) + must(f.SetCellValue(clusterSheetName, "L1", "Memory on top [GiB*h]")) + must(f.SetCellValue(clusterSheetName, "M1", "Memory avg [GiB]")) + must(f.SetCellValue(clusterSheetName, "N1", "Local vol used [GiB*h]")) + must(f.SetCellValue(clusterSheetName, "O1", "Local vol on top [GiB*h]")) + must(f.SetCellValue(clusterSheetName, "P1", "Local vol avg [GiB]")) + must(f.SetColWidth(clusterSheetName, "H", "P", 15)) + for i, v := range responseCluster.Payload.Usage { + must(f.SetCellValue(clusterSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(clusterSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(clusterSheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(clusterSheetName, "D"+fmt.Sprint(i+2), *v.Clusterid)) + must(f.SetCellValue(clusterSheetName, "E"+fmt.Sprint(i+2), *v.Clustername)) + start := time.Time(*v.Clusterstart) + must(f.SetCellValue(clusterSheetName, "F"+fmt.Sprint(i+2), start)) + must(f.SetCellValue(clusterSheetName, "G"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + must(f.SetCellFormula(clusterSheetName, "H"+fmt.Sprint(i+2), "=SUMIF('"+containerSheetName+"'!D$1:D$9999,D"+fmt.Sprint(i+2)+",'"+containerSheetName+"'!M$1:M$99999)/3600")) + must(f.SetCellFormula(clusterSheetName, "I"+fmt.Sprint(i+2), "=MAX(H"+fmt.Sprint(i+2)+"-Parameter!B7*G"+fmt.Sprint(i+2)+"/3600,0)")) + must(f.SetCellFormula(clusterSheetName, "J"+fmt.Sprint(i+2), "=H"+fmt.Sprint(i+2)+"/G"+fmt.Sprint(i+2)+"*3600")) + must(f.SetCellFormula(clusterSheetName, "K"+fmt.Sprint(i+2), "=SUMIF('"+containerSheetName+"'!D$1:D$9999,D"+fmt.Sprint(i+2)+",'"+containerSheetName+"'!N$1:N$9999)/3600")) + must(f.SetCellFormula(clusterSheetName, "L"+fmt.Sprint(i+2), "=MAX(K"+fmt.Sprint(i+2)+"-Parameter!B8*G"+fmt.Sprint(i+2)+"/3600,0)")) + must(f.SetCellFormula(clusterSheetName, "M"+fmt.Sprint(i+2), "=K"+fmt.Sprint(i+2)+"/G"+fmt.Sprint(i+2)+"*3600")) + must(f.SetCellFormula(clusterSheetName, "N"+fmt.Sprint(i+2), "=SUMIF('"+volumeSheetName+"'!D$1:D$9999,D"+fmt.Sprint(i+2)+",'"+volumeSheetName+"'!M$1:M$9999)/3600")) + must(f.SetCellFormula(clusterSheetName, "O"+fmt.Sprint(i+2), "=MAX(N"+fmt.Sprint(i+2)+"-Parameter!B9*G"+fmt.Sprint(i+2)+"/3600,0)")) + must(f.SetCellFormula(clusterSheetName, "P"+fmt.Sprint(i+2), "=N"+fmt.Sprint(i+2)+"/G"+fmt.Sprint(i+2)+"*3600")) + } + + // Container Billing + curContainer := models.V1ContainerUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curContainer.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curContainer.Projectid = billingOpts.ProjectID + } + + requestContainer := accounting.NewContainerUsageParams() + requestContainer.SetBody(&curContainer) + + responseContainer, err := c.cloud.Accounting.ContainerUsage(requestContainer, nil) + if err != nil { + return err + } + must(f.SetCellValue(containerSheetName, "A1", "Tenant")) + must(f.SetCellValue(containerSheetName, "B1", "Project ID")) + must(f.SetColWidth(containerSheetName, "B", "B", 0)) + must(f.SetCellValue(containerSheetName, "C1", "Project")) + must(f.SetCellValue(containerSheetName, "D1", "Cluster ID")) + must(f.SetColWidth(containerSheetName, "D", "D", 0)) + must(f.SetCellValue(containerSheetName, "E1", "Cluster")) + must(f.SetCellValue(containerSheetName, "F1", "Namespace")) + must(f.SetCellValue(containerSheetName, "G1", "Pod ID")) + must(f.SetColWidth(containerSheetName, "G", "G", 0)) + must(f.SetCellValue(containerSheetName, "H1", "Podname")) + must(f.SetCellValue(containerSheetName, "I1", "Containername")) + must(f.SetCellValue(containerSheetName, "J1", "Containerimage")) + must(f.SetCellValue(containerSheetName, "K1", "Start")) + must(f.SetColWidth(containerSheetName, "K", "K", 15)) + must(f.SetCellValue(containerSheetName, "L1", "Lifetime[s]")) + must(f.SetCellValue(containerSheetName, "M1", "CPU[CPU*s]")) + must(f.SetCellValue(containerSheetName, "N1", "Memory[GiB*s]")) + must(f.SetCellValue(containerSheetName, "O1", "Annotations")) + for i, v := range responseContainer.Payload.Usage { + must(f.SetCellValue(containerSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(containerSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(containerSheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(containerSheetName, "D"+fmt.Sprint(i+2), *v.Clusterid)) + must(f.SetCellValue(containerSheetName, "E"+fmt.Sprint(i+2), *v.Clustername)) + must(f.SetCellValue(containerSheetName, "F"+fmt.Sprint(i+2), *v.Namespace)) + must(f.SetCellValue(containerSheetName, "G"+fmt.Sprint(i+2), *v.Poduuid)) + must(f.SetCellValue(containerSheetName, "H"+fmt.Sprint(i+2), *v.Podname)) + must(f.SetCellValue(containerSheetName, "I"+fmt.Sprint(i+2), *v.Containername)) + must(f.SetCellValue(containerSheetName, "J"+fmt.Sprint(i+2), *v.Containerimage)) + start := time.Time(*v.Podstart) + must(f.SetCellValue(containerSheetName, "K"+fmt.Sprint(i+2), start)) + must(f.SetCellValue(containerSheetName, "L"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + cpuseconds, _ := strconv.Atoi(*v.Cpuseconds) + must(f.SetCellValue(containerSheetName, "M"+fmt.Sprint(i+2), cpuseconds)) + memoryseconds, _ := strconv.Atoi(*v.Memoryseconds) + must(f.SetCellValue(containerSheetName, "N"+fmt.Sprint(i+2), memoryseconds/1024/1024/1024)) + must(f.SetCellValue(containerSheetName, "O"+fmt.Sprint(i+2), strings.Join(v.Annotations, "; "))) + } + + // Volume Billing + curVolume := models.V1VolumeUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curVolume.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curVolume.Projectid = billingOpts.ProjectID + } + + requestVolume := accounting.NewVolumeUsageParams() + requestVolume.SetBody(&curVolume) + + responseVolume, err := c.cloud.Accounting.VolumeUsage(requestVolume, nil) + if err != nil { + return err + } + must(f.SetCellValue(volumeSheetName, "A1", "Tenant")) + must(f.SetCellValue(volumeSheetName, "B1", "Project ID")) + must(f.SetColWidth(volumeSheetName, "B", "B", 0)) + must(f.SetCellValue(volumeSheetName, "C1", "Project")) + must(f.SetCellValue(volumeSheetName, "D1", "Cluster ID")) + must(f.SetColWidth(volumeSheetName, "D", "D", 0)) + must(f.SetCellValue(volumeSheetName, "E1", "Cluster")) + must(f.SetCellValue(volumeSheetName, "F1", "Volume ID")) + must(f.SetColWidth(volumeSheetName, "F", "F", 0)) + must(f.SetCellValue(volumeSheetName, "G1", "Volume")) + must(f.SetCellValue(volumeSheetName, "H1", "Class")) + must(f.SetCellValue(volumeSheetName, "I1", "Type")) + must(f.SetCellValue(volumeSheetName, "J1", "Start")) + must(f.SetColWidth(volumeSheetName, "J", "J", 15)) + must(f.SetCellValue(volumeSheetName, "K1", "Lifetime[s]")) + must(f.SetCellValue(volumeSheetName, "L1", "Capacity[GiB*s]")) + must(f.SetCellValue(volumeSheetName, "M1", "Local[GiB*s]")) + must(f.SetCellValue(volumeSheetName, "N1", "Block[GiB*s]")) + for i, v := range responseVolume.Payload.Usage { + must(f.SetCellValue(volumeSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(volumeSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(volumeSheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(volumeSheetName, "D"+fmt.Sprint(i+2), *v.Clusterid)) + must(f.SetCellValue(volumeSheetName, "E"+fmt.Sprint(i+2), *v.Clustername)) + must(f.SetCellValue(volumeSheetName, "F"+fmt.Sprint(i+2), *v.UUID)) + must(f.SetCellValue(volumeSheetName, "G"+fmt.Sprint(i+2), *v.Name)) + must(f.SetCellValue(volumeSheetName, "H"+fmt.Sprint(i+2), *v.Class)) + must(f.SetCellValue(volumeSheetName, "I"+fmt.Sprint(i+2), *v.Type)) + start := time.Time(*v.Start) + must(f.SetCellValue(volumeSheetName, "J"+fmt.Sprint(i+2), start)) + must(f.SetCellValue(volumeSheetName, "K"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + capacityseconds, _ := strconv.Atoi(*v.Capacityseconds) + must(f.SetCellValue(volumeSheetName, "L"+fmt.Sprint(i+2), capacityseconds/1024/1024/1024)) + must(f.SetCellFormula(volumeSheetName, "M"+fmt.Sprint(i+2), "=IF(LEFT(H"+fmt.Sprint(i+2)+",7)=\"csi-lvm\",L"+fmt.Sprint(i+2)+",0)")) + must(f.SetCellFormula(volumeSheetName, "N"+fmt.Sprint(i+2), "=IF(LEFT(H"+fmt.Sprint(i+2)+",10)=\"partition-\",L"+fmt.Sprint(i+2)+",0)")) + } + + // IP Billing + curIP := models.V1IPUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curIP.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curIP.Projectid = billingOpts.ProjectID + } + + requestIP := accounting.NewIPUsageParams() + requestIP.SetBody(&curIP) + + responseIP, err := c.cloud.Accounting.IPUsage(requestIP, nil) + if err != nil { + return err + } + must(f.SetCellValue(ipSheetName, "A1", "Tenant")) + must(f.SetCellValue(ipSheetName, "B1", "Project ID")) + must(f.SetColWidth(ipSheetName, "B", "B", 0)) + must(f.SetCellValue(ipSheetName, "C1", "Project")) + must(f.SetCellValue(ipSheetName, "D1", "IP")) + must(f.SetColWidth(ipSheetName, "D", "D", 14)) + must(f.SetCellValue(ipSheetName, "E1", "Lifetime[s]")) + for i, v := range responseIP.Payload.Usage { + must(f.SetCellValue(ipSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(ipSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(ipSheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(ipSheetName, "D"+fmt.Sprint(i+2), *v.IP)) + must(f.SetCellValue(ipSheetName, "E"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + } + + // network-traffic Billing + curNetwork := models.V1NetworkUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curNetwork.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curNetwork.Tenant = billingOpts.ProjectID + } + + requestNetwork := accounting.NewNetworkUsageParams() + requestNetwork.SetBody(&curNetwork) + + responseNetwork, err := c.cloud.Accounting.NetworkUsage(requestNetwork, nil) + if err != nil { + return err + } + must(f.SetCellValue(networkSheetName, "A1", "Tenant")) + must(f.SetCellValue(networkSheetName, "B1", "Project ID")) + must(f.SetColWidth(networkSheetName, "B", "B", 0)) + must(f.SetCellValue(networkSheetName, "C1", "Project")) + must(f.SetCellValue(networkSheetName, "D1", "Cluster ID")) + must(f.SetColWidth(networkSheetName, "D", "D", 0)) + must(f.SetCellValue(networkSheetName, "E1", "Cluster")) + must(f.SetCellValue(networkSheetName, "F1", "Device")) + must(f.SetCellValue(networkSheetName, "G1", "In[GiB]")) + must(f.SetCellValue(networkSheetName, "H1", "Out[GiB]")) + must(f.SetCellValue(networkSheetName, "I1", "Total[GiB]")) + for i, v := range responseNetwork.Payload.Usage { + must(f.SetCellValue(networkSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(networkSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(networkSheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(networkSheetName, "D"+fmt.Sprint(i+2), *v.Clusterid)) + must(f.SetCellValue(networkSheetName, "E"+fmt.Sprint(i+2), *v.Clustername)) + must(f.SetCellValue(networkSheetName, "F"+fmt.Sprint(i+2), *v.Device)) + in, _ := strconv.Atoi(*v.In) + must(f.SetCellValue(networkSheetName, "G"+fmt.Sprint(i+2), in/1024/1024/1024)) + out, _ := strconv.Atoi(*v.Out) + must(f.SetCellValue(networkSheetName, "H"+fmt.Sprint(i+2), out/1024/1024/1024)) + total, _ := strconv.Atoi(*v.Total) + must(f.SetCellValue(networkSheetName, "I"+fmt.Sprint(i+2), total/1024/1024/1024)) + } + + // S3 Billing + curS3 := models.V1S3UsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curS3.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curS3.Projectid = billingOpts.ProjectID + } + + requestS3 := accounting.NewS3UsageParams() + requestS3.SetBody(&curS3) + + responseS3, err := c.cloud.Accounting.S3Usage(requestS3, nil) + if err != nil { + return err + } + must(f.SetCellValue(s3SheetName, "A1", "Tenant")) + must(f.SetCellValue(s3SheetName, "B1", "Project ID")) + must(f.SetColWidth(s3SheetName, "B", "B", 0)) + must(f.SetCellValue(s3SheetName, "C1", "Project")) + must(f.SetCellValue(s3SheetName, "D1", "Partition")) + must(f.SetCellValue(s3SheetName, "E1", "User")) + must(f.SetCellValue(s3SheetName, "F1", "Bucket ID")) + must(f.SetColWidth(s3SheetName, "F", "F", 0)) + must(f.SetCellValue(s3SheetName, "G1", "Bucket")) + must(f.SetCellValue(s3SheetName, "H1", "Objects")) + must(f.SetCellValue(s3SheetName, "I1", "Capacity[GiB*s]")) + must(f.SetCellValue(s3SheetName, "J1", "Lifetime[s]")) + for i, v := range responseS3.Payload.Usage { + must(f.SetCellValue(s3SheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(s3SheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(s3SheetName, "C"+fmt.Sprint(i+2), *v.Projectname)) + must(f.SetCellValue(s3SheetName, "D"+fmt.Sprint(i+2), *v.Partition)) + must(f.SetCellValue(s3SheetName, "E"+fmt.Sprint(i+2), *v.User)) + must(f.SetCellValue(s3SheetName, "F"+fmt.Sprint(i+2), *v.Bucketid)) + must(f.SetCellValue(s3SheetName, "G"+fmt.Sprint(i+2), *v.Bucketname)) + objects, _ := strconv.Atoi(*v.Currentnumberofobjects) + must(f.SetCellValue(s3SheetName, "H"+fmt.Sprint(i+2), objects)) + capacityseconds, _ := strconv.Atoi(*v.Storageseconds) + must(f.SetCellValue(s3SheetName, "I"+fmt.Sprint(i+2), capacityseconds/1024/1024/1024)) + must(f.SetCellValue(s3SheetName, "J"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + } + + // Postgres Billing + curPostgres := models.V1PostgresUsageRequest{ + From: &fromDT, + To: strfmt.DateTime(to), + } + if billingOpts.Tenant != "" { + curPostgres.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + curPostgres.Projectid = billingOpts.ProjectID + } + + requestPostgres := accounting.NewPostgresUsageParams() + requestPostgres.SetBody(&curPostgres) + + responsePostgres, err := c.cloud.Accounting.PostgresUsage(requestPostgres, nil) + if err != nil { + return err + } + must(f.SetCellValue(postgresSheetName, "A1", "Tenant")) + must(f.SetCellValue(postgresSheetName, "B1", "Project ID")) + must(f.SetColWidth(postgresSheetName, "B", "B", 0)) + must(f.SetCellValue(postgresSheetName, "C1", "Project")) + must(f.SetCellValue(postgresSheetName, "D1", "Postgres ID")) + must(f.SetColWidth(postgresSheetName, "D", "D", 0)) + must(f.SetCellValue(postgresSheetName, "E1", "Description")) + must(f.SetCellValue(postgresSheetName, "F1", "Start")) + must(f.SetColWidth(postgresSheetName, "F", "F", 15)) + must(f.SetCellValue(postgresSheetName, "G1", "CPU[CPU*s]")) + must(f.SetCellValue(postgresSheetName, "H1", "Memory[GiB*s]")) + must(f.SetCellValue(postgresSheetName, "I1", "Storage[GiB*s]")) + must(f.SetCellValue(postgresSheetName, "J1", "Lifetime[s]")) + for i, v := range responsePostgres.Payload.Usage { + must(f.SetCellValue(postgresSheetName, "A"+fmt.Sprint(i+2), *v.Tenant)) + must(f.SetCellValue(postgresSheetName, "B"+fmt.Sprint(i+2), *v.Projectid)) + must(f.SetCellValue(postgresSheetName, "C"+fmt.Sprint(i+2), "")) + must(f.SetCellValue(postgresSheetName, "D"+fmt.Sprint(i+2), *v.Postgresid)) + must(f.SetCellValue(postgresSheetName, "E"+fmt.Sprint(i+2), *v.Postgresdescription)) + start := time.Time(*v.Postgresstart) + must(f.SetCellValue(postgresSheetName, "F"+fmt.Sprint(i+2), start)) + cpuseconds, _ := strconv.Atoi(*v.Cpuseconds) + must(f.SetCellValue(postgresSheetName, "G"+fmt.Sprint(i+2), cpuseconds)) + memoryseconds, _ := strconv.Atoi(*v.Memoryseconds) + must(f.SetCellValue(postgresSheetName, "H"+fmt.Sprint(i+2), memoryseconds/1024/1024/1024)) + storageseconds, _ := strconv.Atoi(*v.Storageseconds) + must(f.SetCellValue(postgresSheetName, "I"+fmt.Sprint(i+2), storageseconds/1024/1024/1024)) + must(f.SetCellValue(postgresSheetName, "J"+fmt.Sprint(i+2), *v.Lifetime/1000000000)) + } + + filename := "" + if billingOpts.Filename != "" { + filename = billingOpts.Filename + if !strings.HasSuffix(filename, ".xlsx") { + filename = filename + ".xlsx" + } + } else { + if billingOpts.Tenant != "" { + filename = fmt.Sprintf("%04d-%02d-%s-billing.xlsx", year, month, billingOpts.Tenant) + } else { + filename = fmt.Sprintf("%04d-%02d-billing.xlsx", year, month) + } + } + + if err := f.SaveAs(filename); err != nil { + return err + } + + fmt.Println("Created " + filename) + + return nil +} + func (c *config) containerUsage() error { from := strfmt.DateTime(billingOpts.From) cur := models.V1ContainerUsageRequest{ diff --git a/go.mod b/go.mod index 98292aa6..4c90e803 100644 --- a/go.mod +++ b/go.mod @@ -34,19 +34,12 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/VividCortex/ewma v1.2.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cheggaaa/pb/v3 v3.0.8 // indirect github.com/coreos/go-oidc/v3 v3.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/emicklei/go-restful-openapi/v2 v2.8.0 // indirect github.com/emicklei/go-restful/v3 v3.7.3 // indirect - github.com/evanphx/json-patch v4.11.0+incompatible // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/go-logr/logr v1.2.2 // indirect - github.com/go-openapi/analysis v0.21.1 // indirect github.com/go-openapi/errors v0.20.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect @@ -57,14 +50,10 @@ require ( github.com/go-openapi/validate v0.20.3 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-stack/stack v1.8.1 // indirect - github.com/goccy/go-json v0.9.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v32 v32.1.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.1 // indirect github.com/gorilla/mux v1.8.0 // indirect @@ -80,32 +69,62 @@ require ( github.com/lestrrat-go/blackmagic v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.0 // indirect github.com/lestrrat-go/iter v1.0.1 // indirect - github.com/lestrrat-go/jwx v1.2.17 // indirect github.com/lestrrat-go/option v1.0.0 // indirect github.com/magiconair/properties v1.8.5 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/metal-stack/security v0.6.3 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nsf/termbox-go v1.1.1 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.17.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/richardlehane/mscfb v1.0.3 // indirect + github.com/richardlehane/msoleps v1.0.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/spf13/afero v1.8.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/grpc v1.43.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible // indirect + sigs.k8s.io/controller-runtime v0.8.3 // indirect +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/emicklei/go-restful-openapi/v2 v2.8.0 // indirect + github.com/evanphx/json-patch v4.11.0+incompatible // indirect + github.com/go-logr/logr v1.2.2 // indirect + github.com/go-openapi/analysis v0.21.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/goccy/go-json v0.9.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/lestrrat-go/jwx v1.2.17 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/nsf/termbox-go v1.1.1 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.17.0 // indirect + github.com/spf13/afero v1.8.0 // indirect + github.com/stretchr/objx v0.3.0 // indirect + github.com/xuri/excelize/v2 v2.5.0 go.mongodb.org/mongo-driver v1.8.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/goleak v1.1.12 // indirect @@ -113,23 +132,12 @@ require ( go.uber.org/zap v1.20.0 // indirect golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 // indirect - google.golang.org/grpc v1.43.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.2 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible // indirect k8s.io/klog/v2 v2.40.1 // indirect k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 // indirect - sigs.k8s.io/controller-runtime v0.8.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) diff --git a/go.sum b/go.sum index f3f51ca2..beed34f6 100644 --- a/go.sum +++ b/go.sum @@ -867,6 +867,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mreiferson/go-options v1.0.0/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -983,6 +985,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= +github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1091,6 +1097,10 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.5.0 h1:nDDVfX0qaDuGjAvb+5zTd0Bxxoqa1Ffv9B4kiE23PTM= +github.com/xuri/excelize/v2 v2.5.0/go.mod h1:rSu0C3papjzxQA3sdK8cU544TebhrPUoTOaGPIh0Q1A= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1212,6 +1222,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1296,6 +1308,7 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=