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	"errors"
22	"strconv"
23
24	"github.com/dustin/go-humanize"
25	"github.com/fatih/color"
26	"github.com/minio/cli"
27	json "github.com/minio/colorjson"
28	"github.com/minio/madmin-go"
29	"github.com/minio/mc/pkg/probe"
30	"github.com/minio/pkg/console"
31)
32
33var adminTierInfoCmd = cli.Command{
34	Name:         "info",
35	Usage:        "Displays per-tier statistics of all tier targets",
36	Action:       mainAdminTierInfo,
37	OnUsageError: onUsageError,
38	Before:       setGlobalsFromContext,
39	Flags:        globalFlags,
40	CustomHelpTemplate: `NAME:
41  {{.HelpName}} - {{.Usage}}
42
43USAGE:
44  {{.HelpName}} TARGET
45
46FLAGS:
47  {{range .VisibleFlags}}{{.}}
48  {{end}}
49
50EXAMPLES:
51  1. Prints per-tier statistics of all remote tier targets configured in myminio
52     {{.Prompt}} {{.HelpName}} myminio
53`,
54}
55
56// checkAdminTierInfoSyntax - validate all the passed arguments
57func checkAdminTierInfoSyntax(ctx *cli.Context) {
58	argsNr := len(ctx.Args())
59	if argsNr < 1 {
60		cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) // last argument is exit code
61	}
62	if argsNr > 1 {
63		fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
64			"Incorrect number of arguments for tier-info subcommand.")
65	}
66}
67
68type tierInfoRowHdr int
69
70const (
71	tierInfoNameHdr tierInfoRowHdr = iota
72	tierInfoAPIHdr
73	tierInfoTypeHdr
74	tierInfoUsageHdr
75	tierInfoObjectsHdr
76	tierInfoVersionsHdr
77)
78
79var tierInfoRowNames = []string{
80	"Tier Name",
81	"API",
82	"Type",
83	"Usage",
84	"Objects",
85	"Versions",
86}
87
88var tierInfoColorScheme = []*color.Color{
89	color.New(color.FgYellow),
90	color.New(color.FgCyan),
91	color.New(color.FgCyan),
92	color.New(color.FgHiWhite),
93	color.New(color.FgHiWhite),
94	color.New(color.FgHiWhite),
95}
96
97type tierInfos []madmin.TierInfo
98
99func (t tierInfos) NumRows() int {
100	return len([]madmin.TierInfo(t))
101}
102
103func (t tierInfos) NumCols() int {
104	return len(tierInfoRowNames)
105}
106func (t tierInfos) EmptyMessage() string {
107	return "No remote tiers configured."
108}
109
110func (t tierInfos) MarshalJSON() ([]byte, error) {
111	type tierInfo struct {
112		Name  string
113		API   string
114		Type  string
115		Stats madmin.TierStats
116	}
117	ts := make([]tierInfo, 0, len(t))
118	for _, tInfo := range t {
119		ts = append(ts, tierInfo{
120			Name:  tInfo.Name,
121			API:   tierInfoAPI(tInfo.Type),
122			Type:  tierInfoType(tInfo.Type),
123			Stats: tInfo.Stats,
124		})
125	}
126	return json.Marshal(ts)
127}
128
129func tierInfoAPI(tierType string) string {
130	switch tierType {
131	case madmin.S3.String(), madmin.GCS.String():
132		return tierType
133	case madmin.Azure.String():
134		return "blob"
135	case "internal":
136		return madmin.S3.String()
137	default:
138		return "unknown"
139	}
140}
141
142func tierInfoType(tierType string) string {
143	if tierType == "internal" {
144		return "hot"
145	}
146	return "warm"
147}
148
149func (t tierInfos) ToRow(i int, ls []int) []string {
150	row := make([]string, len(tierInfoRowNames))
151	if i == -1 {
152		copy(row, tierInfoRowNames)
153	} else {
154		tierInfo := t[i]
155		row[tierInfoNameHdr] = tierInfo.Name
156		row[tierInfoAPIHdr] = tierInfoAPI(tierInfo.Type)
157		row[tierInfoTypeHdr] = tierInfoType(tierInfo.Type)
158		row[tierInfoUsageHdr] = humanize.IBytes(tierInfo.Stats.TotalSize)
159		row[tierInfoObjectsHdr] = strconv.Itoa(tierInfo.Stats.NumObjects)
160		row[tierInfoVersionsHdr] = strconv.Itoa(tierInfo.Stats.NumVersions)
161	}
162
163	// update ls to accommodate this row's values
164	for i := range tierInfoRowNames {
165		if ls[i] < len(row[i]) {
166			ls[i] = len(row[i])
167		}
168	}
169	return row
170}
171
172func mainAdminTierInfo(ctx *cli.Context) error {
173	checkAdminTierInfoSyntax(ctx)
174
175	for i, color := range tierInfoColorScheme {
176		console.SetColor(tierInfoRowNames[i], color)
177	}
178
179	args := ctx.Args()
180	aliasedURL := args.Get(0)
181
182	// Create a new MinIO Admin Client
183	client, cerr := newAdminClient(aliasedURL)
184	fatalIf(cerr, "Unable to initialize admin connection.")
185
186	var msg tierInfoMessage
187	tInfos, err := client.TierStats(globalContext)
188	if err != nil {
189		msg = tierInfoMessage{
190			Status:  "error",
191			Context: ctx,
192			Error:   err.Error(),
193		}
194	} else {
195		msg = tierInfoMessage{
196			Status:    "success",
197			Context:   ctx,
198			TierInfos: tierInfos(tInfos),
199		}
200	}
201	printMsg(&msg)
202	return nil
203}
204
205type tierInfoMessage struct {
206	Status    string       `json:"status"`
207	Context   *cli.Context `json:"-"`
208	TierInfos tierInfos    `json:"tiers,omitempty"`
209	Error     string       `json:"error,omitempty"`
210}
211
212// String method returns a tabular listing of remote tier configurations.
213func (msg *tierInfoMessage) String() string {
214	if msg.Status == "error" {
215		fatal(probe.NewError(errors.New(msg.Error)), "Unable to get tier statistics")
216	}
217	return toTable(tierInfos(msg.TierInfos))
218}
219
220// JSON method returns JSON encoding of msg.
221func (msg *tierInfoMessage) JSON() string {
222	b, _ := json.Marshal(msg)
223	return string(b)
224}
225