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