1/*
2** Zabbix
3** Copyright (C) 2001-2021 Zabbix SIA
4**
5** This program is free software; you can redistribute it and/or modify
6** it under the terms of the GNU General Public License as published by
7** the Free Software Foundation; either version 2 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 General Public License for more details.
14**
15** You should have received a copy of the GNU General Public License
16** along with this program; if not, write to the Free Software
17** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18**/
19
20// Package metric provides an interface for describing a schema of metric's parameters.
21package metric
22
23import (
24	"fmt"
25	"reflect"
26	"strconv"
27	"strings"
28	"unicode"
29
30	"zabbix.com/pkg/zbxerr"
31)
32
33type paramKind int
34
35const (
36	kindSession paramKind = iota
37	kindConn
38	kindGeneral
39	kindSessionOnly
40)
41
42const (
43	required = true
44	optional = false
45)
46
47// Param stores parameters' metadata.
48type Param struct {
49	name         string
50	description  string
51	kind         paramKind
52	required     bool
53	validator    Validator
54	defaultValue *string
55}
56
57func ucFirst(str string) string {
58	for i, v := range str {
59		return string(unicode.ToUpper(v)) + str[i+1:]
60	}
61
62	return ""
63}
64
65func newParam(name, description string, kind paramKind, required bool, validator Validator) *Param {
66	name = strings.TrimSpace(name)
67	if len(name) == 0 {
68		panic("parameter name cannot be empty")
69	}
70
71	description = ucFirst(strings.TrimSpace(description))
72	if len(description) == 0 {
73		panic("parameter description cannot be empty")
74	}
75
76	if description[len(description)-1:] != "." {
77		description += "."
78	}
79
80	return &Param{
81		name:         name,
82		description:  description,
83		kind:         kind,
84		required:     required,
85		validator:    validator,
86		defaultValue: nil,
87	}
88}
89
90// NewParam creates a new parameter with given name and validator.
91// Returns a pointer.
92func NewParam(name, description string) *Param {
93	return newParam(name, description, kindGeneral, optional, nil)
94}
95
96// NewConnParam creates a new connection parameter with given name and validator.
97// Returns a pointer.
98func NewConnParam(name, description string) *Param {
99	return newParam(name, description, kindConn, optional, nil)
100}
101
102// NewSessionParam creates a new connection parameter with given name and validator.
103// Returns a pointer.
104func NewSessionOnlyParam(name, description string) *Param {
105	return newParam(name, description, kindSessionOnly, optional, nil)
106}
107
108// WithSession transforms a connection typed parameter to a dual purpose parameter which can be either
109// a connection parameter or session name.
110// Returns a pointer.
111func (p *Param) WithSession() *Param {
112	if p.kind != kindConn {
113		panic("only connection typed parameter can be transformed to session")
114	}
115
116	p.kind = kindSession
117
118	return p
119}
120
121// WithDefault sets the default value for a parameter.
122// It panics if a default value is specified for a required parameter.
123func (p *Param) WithDefault(value string) *Param {
124	if p.required {
125		panic("default value cannot be applied to a required parameter")
126	}
127
128	p.defaultValue = &value
129
130	return p
131}
132
133// WithValidator sets a validator for a parameter
134func (p *Param) WithValidator(validator Validator) *Param {
135	if validator == nil {
136		panic("validator cannot be nil")
137	}
138
139	p.validator = validator
140
141	if p.defaultValue != nil {
142		if err := p.validator.Validate(p.defaultValue); err != nil {
143			panic(fmt.Sprintf("invalid default value %q for parameter %q: %s",
144				*p.defaultValue, p.name, err.Error()))
145		}
146	}
147
148	return p
149}
150
151// SetRequired makes the parameter mandatory.
152// It panics if default value is specified for required parameter.
153func (p *Param) SetRequired() *Param {
154	if p.defaultValue != nil {
155		panic("required parameter cannot have a default value")
156	}
157
158	p.required = required
159
160	return p
161}
162
163// Metric stores a description of a metric and its parameters.
164type Metric struct {
165	description string
166	params      []*Param
167	varParam    bool
168}
169
170// ordinalize convert a given number to an ordinal numeral.
171func ordinalize(num int) string {
172	var ordinals = map[int]string{
173		1:  "first",
174		2:  "second",
175		3:  "third",
176		4:  "fourth",
177		5:  "fifth",
178		6:  "sixth",
179		7:  "seventh",
180		8:  "eighth",
181		9:  "ninth",
182		10: "tenth",
183	}
184
185	if num >= 1 && num <= 10 {
186		return ordinals[num]
187	}
188
189	suffix := "th"
190	switch num % 10 {
191	case 1:
192		if num%100 != 11 {
193			suffix = "st"
194		}
195	case 2:
196		if num%100 != 12 {
197			suffix = "nd"
198		}
199	case 3:
200		if num%100 != 13 {
201			suffix = "rd"
202		}
203	}
204
205	return strconv.Itoa(num) + suffix
206}
207
208// New creates an instance of a Metric and returns a pointer to it.
209// It panics if a metric is not satisfied to one of the following rules:
210// 1. Parameters must be named (and names must be unique).
211// 2. It's forbidden to duplicate parameters' names.
212// 3. Session must be placed first.
213// 4. Connection parameters must be placed in a row.
214func New(description string, params []*Param, varParam bool) *Metric {
215	connParamIdx := -1
216
217	description = ucFirst(strings.TrimSpace(description))
218	if len(description) == 0 {
219		panic("metric description cannot be empty")
220	}
221
222	if description[len(description)-1:] != "." {
223		description += "."
224	}
225
226	if params == nil {
227		params = []*Param{}
228	}
229
230	if len(params) > 0 {
231		if params[0].kind != kindGeneral {
232			connParamIdx = 0
233		}
234	}
235
236	paramsMap := make(map[string]bool)
237
238	for i, p := range params {
239		if _, exists := paramsMap[p.name]; exists {
240			panic(fmt.Sprintf("name of parameter %q must be unique", p.name))
241		}
242
243		paramsMap[p.name] = true
244
245		if i > 0 && p.kind == kindSession {
246			panic("session must be placed first")
247		}
248
249		if p.kind == kindConn {
250			if i-connParamIdx > 1 {
251				panic("parameters describing a connection must be placed in a row")
252			}
253
254			connParamIdx = i
255		}
256	}
257
258	return &Metric{
259		description: description,
260		params:      params,
261		varParam:    varParam,
262	}
263}
264
265func findSession(name string, sessions interface{}) (session interface{}) {
266	v := reflect.ValueOf(sessions)
267	if v.Kind() != reflect.Map {
268		panic("sessions must be map of strings")
269	}
270
271	for _, key := range v.MapKeys() {
272		if name == key.String() {
273			session = v.MapIndex(key).Interface()
274			break
275		}
276	}
277
278	return
279}
280
281func mergeWithSessionData(out map[string]string, metricParams []*Param, session interface{}) error {
282	v := reflect.ValueOf(session)
283	for i := 0; i < v.NumField(); i++ {
284		var p *Param = nil
285
286		val := v.Field(i).String()
287
288		j := 0
289		for j = range metricParams {
290			if metricParams[j].name == v.Type().Field(i).Name {
291				p = metricParams[j]
292				break
293			}
294		}
295
296		ordNum := ordinalize(j + 1)
297
298		if p == nil {
299			panic(fmt.Sprintf("cannot find parameter %q in schema", v.Type().Field(i).Name))
300		}
301
302		if val == "" {
303			if p.required {
304				return zbxerr.ErrorTooFewParameters.Wrap(
305					fmt.Errorf("%s parameter %q is required", ordNum, p.name))
306			}
307
308			if p.defaultValue != nil {
309				val = *p.defaultValue
310			}
311		}
312
313		if p.validator != nil {
314			if err := p.validator.Validate(&val); err != nil {
315				return zbxerr.New(fmt.Sprintf("invalid %s parameter %q", ordNum, p.name)).Wrap(err)
316			}
317		}
318
319		out[p.name] = val
320	}
321
322	return nil
323}
324
325// EvalParams returns a mapping of parameters' names to their values passed to a plugin and/or
326// sessions specified in the configuration file.
327// If a session is configured, then an other connection parameters must not be accepted and an error will be returned.
328// Also it returns error in following cases:
329// * incorrect number of parameters are passed;
330// * missing required parameter;
331// * value validation is failed.
332func (m *Metric) EvalParams(rawParams []string, sessions interface{}) (params map[string]string, err error) {
333	var (
334		session interface{}
335		val     *string
336	)
337
338	if !m.varParam && len(rawParams) > len(m.params) {
339		return nil, zbxerr.ErrorTooManyParameters
340	}
341
342	if len(rawParams) > 0 && m.params[0].kind == kindSession {
343		session = findSession(rawParams[0], sessions)
344	}
345
346	params = make(map[string]string)
347
348	for i, p := range m.params {
349		kind := p.kind
350		if kind == kindSession {
351			if session != nil {
352				continue
353			}
354
355			kind = kindConn
356		}
357
358		val = nil
359		skipConnIfSessionIsSet := !(session != nil && kind == kindConn)
360		ordNum := ordinalize(i + 1)
361
362		if i >= len(rawParams) || rawParams[i] == "" {
363			if p.required && skipConnIfSessionIsSet {
364				return nil, zbxerr.ErrorTooFewParameters.Wrap(
365					fmt.Errorf("%s parameter %q is required", ordNum, p.name))
366			}
367
368			if p.defaultValue != nil && skipConnIfSessionIsSet {
369				val = p.defaultValue
370			}
371		} else {
372			if p.kind == kindSessionOnly {
373				return nil, zbxerr.ErrorInvalidParams.Wrap(
374					fmt.Errorf("%q cannot be passed as a key parameter", p.name))
375			}
376			val = &rawParams[i]
377		}
378
379		if val == nil {
380			continue
381		}
382
383		if p.validator != nil && skipConnIfSessionIsSet {
384			if err = p.validator.Validate(val); err != nil {
385				return nil, zbxerr.New(fmt.Sprintf("invalid %s parameter %q", ordNum, p.name)).Wrap(err)
386			}
387		}
388
389		if kind == kindConn {
390			if session == nil {
391				params[p.name] = *val
392			} else {
393				return nil, zbxerr.ErrorInvalidParams.Wrap(
394					fmt.Errorf("%s parameter %q cannot be passed along with session", ordNum, p.name))
395			}
396		}
397
398		if kind == kindGeneral {
399			params[p.name] = *val
400		}
401	}
402
403	// Fill connection parameters with data from a session
404	if session != nil {
405		if err = mergeWithSessionData(params, m.params, session); err != nil {
406			return nil, err
407		}
408
409		params["sessionName"] = rawParams[0]
410	}
411
412	return params, nil
413}
414
415// MetricSet stores a mapping of keys to metrics.
416type MetricSet map[string]*Metric
417
418// List returns an array of metrics' keys and their descriptions suitable to pass to plugin.RegisterMetrics.
419func (ml MetricSet) List() (list []string) {
420	for key, metric := range ml {
421		list = append(list, key, metric.description)
422	}
423
424	return
425}
426