1package command
2
3import (
4	"encoding/base64"
5	"fmt"
6	"sort"
7	"strconv"
8	"strings"
9
10	"github.com/hashicorp/nomad/api"
11	"github.com/posener/complete"
12)
13
14type QuotaStatusCommand struct {
15	Meta
16}
17
18func (c *QuotaStatusCommand) Help() string {
19	helpText := `
20Usage: nomad quota status [options] <quota>
21
22  Status is used to view the status of a particular quota specification.
23
24General Options:
25
26  ` + generalOptionsUsage()
27
28	return strings.TrimSpace(helpText)
29}
30
31func (c *QuotaStatusCommand) AutocompleteFlags() complete.Flags {
32	return c.Meta.AutocompleteFlags(FlagSetClient)
33}
34
35func (c *QuotaStatusCommand) AutocompleteArgs() complete.Predictor {
36	return QuotaPredictor(c.Meta.Client)
37}
38
39func (c *QuotaStatusCommand) Synopsis() string {
40	return "Display a quota's status and current usage"
41}
42
43func (c *QuotaStatusCommand) Name() string { return "quota status" }
44
45func (c *QuotaStatusCommand) Run(args []string) int {
46	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
47	flags.Usage = func() { c.Ui.Output(c.Help()) }
48
49	if err := flags.Parse(args); err != nil {
50		return 1
51	}
52
53	// Check that we got one arguments
54	args = flags.Args()
55	if l := len(args); l != 1 {
56		c.Ui.Error("This command takes one argument: <quota>")
57		c.Ui.Error(commandErrorText(c))
58		return 1
59	}
60
61	name := args[0]
62
63	// Get the HTTP client
64	client, err := c.Meta.Client()
65	if err != nil {
66		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
67		return 1
68	}
69
70	// Do a prefix lookup
71	quotas := client.Quotas()
72	spec, possible, err := getQuota(quotas, name)
73	if err != nil {
74		c.Ui.Error(fmt.Sprintf("Error retrieving quota: %s", err))
75		return 1
76	}
77
78	if len(possible) != 0 {
79		c.Ui.Error(fmt.Sprintf("Prefix matched multiple quotas\n\n%s", formatQuotaSpecs(possible)))
80		return 1
81	}
82
83	// Format the basics
84	c.Ui.Output(formatQuotaSpecBasics(spec))
85
86	// Get the quota usages
87	usages, failures := quotaUsages(spec, quotas)
88
89	// Format the limits
90	c.Ui.Output(c.Colorize().Color("\n[bold]Quota Limits[reset]"))
91	c.Ui.Output(formatQuotaLimits(spec, usages))
92
93	// Display any failures
94	if len(failures) != 0 {
95		c.Ui.Error(c.Colorize().Color("\n[bold][red]Lookup Failures[reset]"))
96		for region, failure := range failures {
97			c.Ui.Error(fmt.Sprintf("  * Failed to retrieve quota usage for region %q: %v", region, failure))
98			return 1
99		}
100	}
101
102	return 0
103}
104
105// quotaUsages returns the quota usages for the limits described by the spec. It
106// will make a request to each referenced Nomad region. If the region couldn't
107// be contacted, the error will be stored in the failures map
108func quotaUsages(spec *api.QuotaSpec, client *api.Quotas) (usages map[string]*api.QuotaUsage, failures map[string]error) {
109	// Determine the regions we have limits for
110	regions := make(map[string]struct{})
111	for _, limit := range spec.Limits {
112		regions[limit.Region] = struct{}{}
113	}
114
115	usages = make(map[string]*api.QuotaUsage, len(regions))
116	failures = make(map[string]error)
117	q := api.QueryOptions{}
118
119	// Retrieve the usage per region
120	for region := range regions {
121		q.Region = region
122		usage, _, err := client.Usage(spec.Name, &q)
123		if err != nil {
124			failures[region] = err
125			continue
126		}
127
128		usages[region] = usage
129	}
130
131	return usages, failures
132}
133
134// formatQuotaSpecBasics formats the basic information of the quota
135// specification.
136func formatQuotaSpecBasics(spec *api.QuotaSpec) string {
137	basic := []string{
138		fmt.Sprintf("Name|%s", spec.Name),
139		fmt.Sprintf("Description|%s", spec.Description),
140		fmt.Sprintf("Limits|%d", len(spec.Limits)),
141	}
142
143	return formatKV(basic)
144}
145
146// formatQuotaLimits formats the limits to display the quota usage versus the
147// limit per quota limit. It takes as input the specification as well as quota
148// usage by region. The formatter handles missing usages.
149func formatQuotaLimits(spec *api.QuotaSpec, usages map[string]*api.QuotaUsage) string {
150	if len(spec.Limits) == 0 {
151		return "No quota limits defined"
152	}
153
154	// Sort the limits
155	sort.Sort(api.QuotaLimitSort(spec.Limits))
156
157	limits := make([]string, len(spec.Limits)+1)
158	limits[0] = "Region|CPU Usage|Memory Usage|Network Usage"
159	i := 0
160	for _, specLimit := range spec.Limits {
161		i++
162
163		// lookupUsage returns the regions quota usage for the limit
164		lookupUsage := func() (*api.QuotaLimit, bool) {
165			usage, ok := usages[specLimit.Region]
166			if !ok {
167				return nil, false
168			}
169
170			used, ok := usage.Used[base64.StdEncoding.EncodeToString(specLimit.Hash)]
171			return used, ok
172		}
173
174		specBits := 0
175		if len(specLimit.RegionLimit.Networks) == 1 {
176			specBits = *specLimit.RegionLimit.Networks[0].MBits
177		}
178
179		used, ok := lookupUsage()
180		if !ok {
181			cpu := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.CPU))
182			memory := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB))
183			net := fmt.Sprintf("- / %s", formatQuotaLimitInt(&specBits))
184			limits[i] = fmt.Sprintf("%s|%s|%s|%s", specLimit.Region, cpu, memory, net)
185			continue
186		}
187
188		cpu := fmt.Sprintf("%d / %s", *used.RegionLimit.CPU, formatQuotaLimitInt(specLimit.RegionLimit.CPU))
189		memory := fmt.Sprintf("%d / %s", *used.RegionLimit.MemoryMB, formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB))
190		net := fmt.Sprintf("- / %s", formatQuotaLimitInt(&specBits))
191		if len(used.RegionLimit.Networks) == 1 {
192			net = fmt.Sprintf("%d / %s", *used.RegionLimit.Networks[0].MBits, formatQuotaLimitInt(&specBits))
193		}
194		limits[i] = fmt.Sprintf("%s|%s|%s|%s", specLimit.Region, cpu, memory, net)
195	}
196
197	return formatList(limits)
198}
199
200// formatQuotaLimitInt takes a integer resource value and returns the
201// appropriate string for output.
202func formatQuotaLimitInt(value *int) string {
203	if value == nil {
204		return "-"
205	}
206
207	v := *value
208	if v < 0 {
209		return "0"
210	} else if v == 0 {
211		return "inf"
212	}
213
214	return strconv.Itoa(v)
215}
216
217func getQuota(client *api.Quotas, quota string) (match *api.QuotaSpec, possible []*api.QuotaSpec, err error) {
218	// Do a prefix lookup
219	quotas, _, err := client.PrefixList(quota, nil)
220	if err != nil {
221		return nil, nil, err
222	}
223
224	l := len(quotas)
225	switch {
226	case l == 0:
227		return nil, nil, fmt.Errorf("Quota %q matched no quotas", quota)
228	case l == 1:
229		return quotas[0], nil, nil
230	default:
231		return nil, quotas, nil
232	}
233}
234