1// Copyright (c) 2015-2021 MinIO, Inc.
2//
3// This file is part of MinIO Object Storage stack
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18package cmd
19
20import (
21	"fmt"
22	"sort"
23	"strings"
24	"time"
25
26	"github.com/dustin/go-humanize"
27	"github.com/fatih/color"
28	"github.com/minio/cli"
29	json "github.com/minio/colorjson"
30	"github.com/minio/madmin-go"
31	"github.com/minio/mc/pkg/probe"
32	"github.com/minio/pkg/console"
33)
34
35var adminBandwidthInfoCmdFlags = []cli.Flag{
36	cli.StringFlag{
37		Name:  "unit",
38		Value: "b",
39		Usage: "[b|bi|B|Bi] Display bandwidth in bits (IEC [bi] or SI [b]) or bytes (IEC [Bi] or SI [B])",
40	},
41}
42
43var adminBwInfoCmd = cli.Command{
44	Name:         "bandwidth",
45	Usage:        "Show bandwidth info for buckets on the MinIO server in bits or bytes per second. Ki,Bi,Mi,Gi represent IEC units.",
46	Action:       mainAdminBwInfo,
47	Before:       setGlobalsFromContext,
48	OnUsageError: onUsageError,
49	Flags:        append(globalFlags, adminBandwidthInfoCmdFlags...),
50	CustomHelpTemplate: `NAME:
51  {{.HelpName}} - {{.Usage}}
52
53USAGE:
54  {{.HelpName}} FLAGS TARGET
55
56FLAGS:
57  {{range .VisibleFlags}}{{.}}
58  {{end}}
59EXAMPLES:
60  1. Show the bandwidth usage for all the buckets in a MinIO server setup
61     {{.Prompt}} {{.HelpName}} play/
62  2. Show the bandwidth usage for the bucket 'source-bucket' in a MinIO server setup
63     {{.Prompt}} {{.HelpName}} play/source-bucket
64`,
65}
66
67func printTable(report madmin.Report, bits bool, iec bool) {
68	bucketMaxLength := 63
69	bucketColLength := 6
70	var bucketKeys []string
71	for bucket := range report.Report.BucketStats {
72		if len(bucket) <= bucketMaxLength && len(bucket) > bucketColLength {
73			bucketColLength = len(bucket)
74		}
75		bucketKeys = append(bucketKeys, bucket)
76	}
77	sort.Strings(bucketKeys)
78	dspOrder := []col{colGreen} // Header
79	for i := 0; i < len(report.Report.BucketStats); i++ {
80		dspOrder = append(dspOrder, colGrey)
81	}
82	var printColors []*color.Color
83	for _, c := range dspOrder {
84		printColors = append(printColors, getPrintCol(c))
85	}
86
87	cellText := make([][]string, len(report.Report.BucketStats)+1) // 1 for the header
88	tbl := console.NewTable(printColors, []bool{false, false, false}, 0)
89	bucketTitle := fmt.Sprintf("%-16v", "Bucket")
90	cellText[0] = []string{
91		bucketTitle,
92		"Configured Max Bandwidth",
93		"Current Bandwidth",
94	}
95	tbl.HeaderRowSeparator = true
96	index := 1
97
98	for _, bucket := range bucketKeys {
99		values := report.Report.BucketStats[bucket]
100		if len(bucket) > bucketMaxLength {
101			bucket = bucket[:bucketMaxLength] + ".."
102		}
103		var mul uint64
104		mul = 1
105		if bits {
106			mul = 8
107		}
108		limit := humanize.Bytes(uint64(values.LimitInBytesPerSecond) * mul)
109		current := humanize.Bytes(uint64(values.CurrentBandwidthInBytesPerSecond) * mul)
110		if iec {
111			limit = humanize.IBytes(uint64(values.LimitInBytesPerSecond)*mul) + "/sec"
112			current = humanize.IBytes(uint64(values.CurrentBandwidthInBytesPerSecond)*mul) + "/sec"
113		}
114		if bits {
115			limit = strings.ToLower(limit) + "/sec"
116			current = strings.ToLower(current) + "/sec"
117		}
118		if values.LimitInBytesPerSecond == 0 {
119			limit = "N/A" // N/A means cluster bandwidth is not configured
120		}
121		cellText[index] = []string{
122			bucket,
123			limit,
124			current,
125		}
126		index++
127	}
128	if len(report.Report.BucketStats) > 0 {
129		err := tbl.DisplayTable(cellText)
130		if err != nil {
131			console.Error(err)
132		}
133	}
134}
135func checkAdminBwInfoSyntax(ctx *cli.Context) {
136	u := ctx.String("unit")
137	if u != "bi" &&
138		u != "b" &&
139		u != "Bi" &&
140		u != "B" &&
141		u != "" {
142		cli.ShowCommandHelpAndExit(ctx, "bandwidth", globalErrorExitStatus)
143	}
144	if len(ctx.Args()) > 1 || len(ctx.Args()) == 0 {
145		cli.ShowCommandHelpAndExit(ctx, "bandwidth", globalErrorExitStatus)
146	}
147}
148
149func mainAdminBwInfo(ctx *cli.Context) {
150	checkAdminBwInfoSyntax(ctx)
151	aliasURL, bucket := getAliasAndBucket(ctx)
152	client := getClient(aliasURL)
153	reportCh := client.GetBucketBandwidth(globalContext, bucket)
154	firstPrint := true
155	bandwidthUnitsString := ctx.String("unit")
156	for {
157		select {
158		case report := <-reportCh:
159			if len(report.Report.BucketStats) == 0 {
160				continue
161			}
162			if report.Err != nil {
163				if strings.Contains(report.Err.Error(), "EOF") {
164					continue
165				}
166				console.Error(report.Err)
167			}
168			printBandwidth(report, firstPrint, bandwidthUnitsString == "bi" || bandwidthUnitsString == "b",
169				bandwidthUnitsString == "bi" || bandwidthUnitsString == "Bi")
170			firstPrint = false
171		case <-globalContext.Done():
172			return
173		}
174	}
175}
176
177func printBandwidth(report madmin.Report, firstPrint bool, bits bool, iec bool) {
178	rewindLines := len(report.Report.BucketStats) + 4
179	if firstPrint {
180		rewindLines = 0
181	}
182	if globalJSON {
183		reportJSON, e := json.MarshalIndent(report, "", "  ")
184		fatalIf(probe.NewError(e), "Unable to marshal to JSON")
185		console.Println(string(reportJSON))
186		time.Sleep(1 * time.Second)
187		return
188	}
189	if len(report.Report.BucketStats) > 0 {
190		console.RewindLines(rewindLines)
191		// For the next iteration, rewind lines
192		printTable(report, bits, iec)
193	}
194	time.Sleep(1 * time.Second)
195}
196