get_metrics.py 21.8 KB
Newer Older
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
1
2
3
# ---------------------------------------------------------------------
# Generic.get_metrics
# ---------------------------------------------------------------------
Dmitry Volodin's avatar
Dmitry Volodin committed
4
# Copyright (C) 2007-2020 The NOC Project
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
5
6
# See LICENSE for details
# ---------------------------------------------------------------------
Dmitry Volodin's avatar
Dmitry Volodin committed
7

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
8
# Python modules
Dmitry Volodin's avatar
Dmitry Volodin committed
9
import time
10
11
import os
import re
12
13
import itertools
import operator
14
from typing import Union, Optional, List, Tuple, Callable, Dict
15
from collections import defaultdict
Dmitry Volodin's avatar
Dmitry Volodin committed
16

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
17
# Third-party modules
Dmitry Volodin's avatar
Dmitry Volodin committed
18
import orjson
Dmitry Volodin's avatar
Dmitry Volodin committed
19

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
20
# NOC modules
21
from noc.core.script.base import BaseScript, BaseScriptMetaclass
Dmitry Volodin's avatar
Dmitry Volodin committed
22
from noc.sa.interfaces.igetmetrics import IGetMetrics
23
24
25
26
27
28
29
30
31
32
33
from noc.core.script.oidrules.oid import OIDRule
from noc.core.script.oidrules.bool import BooleanRule
from noc.core.script.oidrules.capindex import CapabilityIndexRule
from noc.core.script.oidrules.caplist import CapabilityListRule
from noc.core.script.oidrules.caps import CapabilityRule
from noc.core.script.oidrules.counter import CounterRule
from noc.core.script.oidrules.hires import HiresRule
from noc.core.script.oidrules.ifindex import InterfaceRule
from noc.core.script.oidrules.match import MatcherRule
from noc.core.script.oidrules.oids import OIDsRule
from noc.core.script.oidrules.loader import load_rule, with_resolver
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
34
from noc.config import config
35
from noc.core.perf import metrics as noc_metrics
36
from noc.core.mib import mib
Dmitry Volodin's avatar
Dmitry Volodin committed
37

38
NS = 1000000000.0
39
SNMP_OVERLOAD_VALUE = 18446744073709551615  # '0xffffffffffffffff' for 64-bit counter
40
PROFILES_PATH = os.path.join("sa", "profiles")
41

42

43
class MetricConfig(object):
44
    __slots__ = ("id", "metric", "labels", "oid", "ifindex", "sla_type")
45

46
    def __init__(self, id, metric, labels=None, oid=None, ifindex=None, sla_type=None):
47
48
49
50
51
52
        self.id: int = id
        self.metric: str = metric
        self.labels: List[str] = labels
        self.oid: str = oid
        self.ifindex: int = ifindex
        self.sla_type: str = sla_type
53
54
55
56
57
58

    def __repr__(self):
        return "<MetricConfig #%s %s>" % (self.id, self.metric)


class BatchConfig(object):
Dmitry Volodin's avatar
Dmitry Volodin committed
59
    __slots__ = ("id", "metric", "labels", "type", "scale")
60

Dmitry Volodin's avatar
Dmitry Volodin committed
61
    def __init__(self, id, metric, labels, type, scale):
62
63
64
65
        self.id: int = id
        self.metric: str = metric
        self.labels: List[str] = labels
        self.type: str = type
66
67
68
        self.scale = scale


69
70
# Internal sequence number for @metrics decorator ordering
_mt_seq = itertools.count(0)
71
72


Dmitry Volodin's avatar
Dmitry Volodin committed
73
74
75
def metrics(
    metrics, has_script=None, has_capability=None, matcher=None, access=None, volatile=True
):
76
    """
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    Decorator to use inside get_metrics script to denote functions
    which can return set of metrics

    @metrics(["Metric Type1", "Metric Type2"])
    def get_custom_metrics(self, metrics):
        ...
        self.set_metric(...)
        ...

    NB: set_metric() function applies metrics to local context.
    @metrics decorator maps requested and applicable metrics to global result
    Handler accepts *metrics* parameter containing list of MetricConfig
    applicable to its types

    :param metrics: string or list of metric type names
    :param has_script: Match only if object has script
    :param has_capability: Match only if object has capability
    :param matcher: Match only if object fits to matcher
    :param access: Required access. Should be
        * S - for SNMP-only
        * C - for CLI-only
        * None - always match
Dmitry Volodin's avatar
Dmitry Volodin committed
99
100
101
    :param volatile: True - Function call results may be changed over time
        False - Function call results are persistent.
        Function may be called only once
102
    :return: None
103
    """
Dmitry Volodin's avatar
Dmitry Volodin committed
104

105
106
107
108
109
110
111
    def wrapper(f):
        f.mt_seq = next(_mt_seq)
        f.mt_metrics = metrics
        f.mt_has_script = has_script
        f.mt_has_capability = has_capability
        f.mt_matcher = matcher
        f.mt_access = access
Dmitry Volodin's avatar
Dmitry Volodin committed
112
        f.mt_volatile = volatile
113
114
        return f

115
    if isinstance(metrics, str):
116
117
118
119
120
121
122
123
124
        metrics = [metrics]
    assert isinstance(metrics, list), "metrics must be string or list"
    return wrapper


class MetricScriptBase(BaseScriptMetaclass):
    """
    get_metrics metaclass. Performs @metrics decorator processing
    """
Dmitry Volodin's avatar
Dmitry Volodin committed
125

126
127
128
129
130
131
    def __new__(mcs, name, bases, attrs):
        m = super(MetricScriptBase, mcs).__new__(mcs, name, bases, attrs)
        # Inject metric_type -> [handler] mappings
        m._mt_map = defaultdict(list)
        # Get @metrics handlers
        for h in sorted(
Dmitry Volodin's avatar
Dmitry Volodin committed
132
            (getattr(m, n) for n in dir(m) if hasattr(getattr(m, n), "mt_seq")),
133
            key=operator.attrgetter("mt_seq"),
Dmitry Volodin's avatar
Dmitry Volodin committed
134
            reverse=True,
135
136
137
138
139
140
141
142
        ):
            for mt in h.mt_metrics:
                m._mt_map[mt] += [h]
        # Install oid rules
        # Instantiate from base class' OID_RULES
        parent_rules = getattr(bases[0], "_oid_rules", None)
        if parent_rules:
            m._oid_rules = parent_rules.copy()
143
        else:
144
145
            m._oid_rules = {}
        # Append own rules from OID_RULES
Dmitry Volodin's avatar
Dmitry Volodin committed
146
        m._oid_rules.update({r.name: r for r in m.OID_RULES})
147
148
149
150
        # Load snmp_metrics/*.json
        with with_resolver(m.get_oid_rule):
            mcs.apply_snmp_rules(m)
        return m
151

152
153
    @classmethod
    def apply_snmp_rules(mcs, script):
154
        """
155
156
        Initialize SNMP rules from JSON
        :param script: Script class
157
158
        :return:
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
159

160
        def sort_path_key(s):
161
            """
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
162
            M - Main, C - Custom, G - Generic, P - profile
163
            \\|G|P
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
164
165
166
            -+-+-
            M|3|1
            C|2|0
167
168
169
170
            :param s:
            :return:
            """
            if s.startswith(PROFILES_PATH):
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
171
172
173
                return 3 if "Generic" in s else 1
            else:
                return 2 if "Generic" in s else 0
Dmitry Volodin's avatar
Dmitry Volodin committed
174

175
176
        pp = script.name.rsplit(".", 1)[0]
        if pp == "Generic":
Dmitry Volodin's avatar
Dmitry Volodin committed
177
178
179
180
181
182
            paths = [
                p
                for p in config.get_customized_paths(
                    os.path.join("sa", "profiles", "Generic", "snmp_metrics")
                )
            ]
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
183
        else:
184
            v, p = pp.split(".")
Dmitry Volodin's avatar
Dmitry Volodin committed
185
186
187
188
189
190
191
            paths = sorted(
                config.get_customized_paths(
                    os.path.join("sa", "profiles", "Generic", "snmp_metrics")
                )
                + config.get_customized_paths(os.path.join("sa", "profiles", v, p, "snmp_metrics")),
                key=sort_path_key,
            )
192
193
194
195
196
197
        for path in paths:
            if not os.path.exists(path):
                continue
            for root, dirs, files in os.walk(path):
                for f in files:
                    if f.endswith(".json"):
Dmitry Volodin's avatar
Dmitry Volodin committed
198
                        mcs.apply_snmp_rules_from_json(script, os.path.join(root, f))
199
200

    @classmethod
201
202
203
204
205
    def apply_snmp_rules_from_json(mcs, script, path):
        # @todo: Check read access
        with open(path) as f:
            data = f.read()
        try:
Dmitry Volodin's avatar
Dmitry Volodin committed
206
            data = orjson.loads(data)
207
        except ValueError as e:
Dmitry Volodin's avatar
Dmitry Volodin committed
208
            raise ValueError("Failed to parse file '%s': %s" % (path, e))
Dmitry Volodin's avatar
Dmitry Volodin committed
209
        if not isinstance(data, dict):
Dmitry Volodin's avatar
Dmitry Volodin committed
210
            raise ValueError("Error in file '%s': Must be defined as object" % path)
211
212
213
214
215
        if "$metric" not in data:
            raise ValueError("$metric key is required")
        script._mt_map[data["$metric"]] += [
            mcs.get_snmp_handler(script, data["$metric"], load_rule(data))
        ]
216
217

    @classmethod
218
    def get_snmp_handler(mcs, script, metric, rule):
219
        """
220
        Generate SNMP handler for @metrics
221
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
222

223
224
        def f(self, metrics):
            self.schedule_snmp_oids(rule, metric, metrics)
225

226
227
228
229
230
        fn = mcs.get_snmp_handler_name(metric)
        f.mt_has_script = None
        f.mt_has_capability = "SNMP"
        f.mt_matcher = None
        f.mt_access = "S"
Dmitry Volodin's avatar
Dmitry Volodin committed
231
        f.mt_volatile = False
Dmitry Volodin's avatar
Dmitry Volodin committed
232
        setattr(script, fn, f)
233
        ff = getattr(script, fn)
234
235
        ff.__name__ = fn
        ff.__qualname__ = "%s.%s" % (script.__name__, fn)
236
        return ff
237

238
    rx_mt_name = re.compile("[^a-z0-9]+")
239
240

    @classmethod
241
242
243
244
245
246
247
    def get_snmp_handler_name(mcs, metric):
        """
        Generate python function name
        :param metric:
        :return:
        """
        return "get_snmp_json_%s" % mcs.rx_mt_name.sub("_", str(metric.lower()))
248

Dmitry Volodin's avatar
Dmitry Volodin committed
249

Dmitry Volodin's avatar
Dmitry Volodin committed
250
class Script(BaseScript, metaclass=MetricScriptBase):
Dmitry Volodin's avatar
Dmitry Volodin committed
251
252
253
    """
    Retrieve data for topology discovery
    """
Dmitry Volodin's avatar
Dmitry Volodin committed
254

255
    name = "Generic.get_metrics"
Dmitry Volodin's avatar
Dmitry Volodin committed
256
    interface = IGetMetrics
257
    requires = []
Dmitry Volodin's avatar
Dmitry Volodin committed
258

259
    # Define counter types
260
261
    GAUGE = "gauge"
    COUNTER = "counter"
262
263
264
265
266
267
268
269
270
271
272

    OID_RULES = [
        OIDRule,
        BooleanRule,
        CapabilityIndexRule,
        CapabilityListRule,
        CapabilityRule,
        CounterRule,
        HiresRule,
        InterfaceRule,
        MatcherRule,
Dmitry Volodin's avatar
Dmitry Volodin committed
273
        OIDsRule,
274
    ]
275

Dmitry Volodin's avatar
Dmitry Volodin committed
276
    def __init__(self, *args, **kwargs):
Dmitry Volodin's avatar
Dmitry Volodin committed
277
        super().__init__(*args, **kwargs)
Dmitry Volodin's avatar
Dmitry Volodin committed
278
        self.metrics = []
279
        self.ts: Optional[int] = None
280
281
        # SNMP batch to be collected by collect_snmp_metrics
        # oid -> BatchConfig
282
        self.snmp_batch: Dict[str, List[BatchConfig]] = defaultdict(list)
283
284
        # Collected metric ids
        self.seen_ids = set()
Dmitry Volodin's avatar
Dmitry Volodin committed
285
        # get_labels_hash(metric type, labels) -> metric config
286
        self.labels: Dict[str, List[MetricConfig]] = {}
287
        # metric type -> [metric config]
288
        self.metric_configs: Dict[str, List[MetricConfig]] = defaultdict(list)
Dmitry Volodin's avatar
Dmitry Volodin committed
289

290
    def get_snmp_metrics_get_timeout(self) -> int:
291
292
        """
        Timeout for snmp GET request
293
        :return:
294
295
296
        """
        return self.profile.snmp_metrics_get_timeout

297
    def get_snmp_metrics_get_chunk(self) -> int:
298
299
300
        """
        Aggregate up to *snmp_metrics_get_chunk* oids
        to one SNMP GET request
301
        :return:
302
303
304
        """
        return self.profile.snmp_metrics_get_chunk

305
    @staticmethod
306
    def get_labels_hash(metric: str, labels: List[str]):
Dmitry Volodin's avatar
Dmitry Volodin committed
307
308
        if labels:
            return "\x00".join([metric] + labels)
Dmitry Volodin's avatar
Dmitry Volodin committed
309
310
        else:
            return metric
311

312
    def execute(self, metrics):
313
        """
314
315
316
        Metrics is a list of:
        * id -- Opaque id, must be returned back
        * metric -- Metric type
Dmitry Volodin's avatar
Dmitry Volodin committed
317
        * labels -- metric labels
318
        * oid -- optional oid
319
320
        * ifindex - optional ifindex
        * sla_test - optional sla test inventory
321
        """
322
        # Generate list of MetricConfig from input parameters
323
        metrics: List[MetricConfig] = [MetricConfig(**m) for m in metrics]
324
        # Split by metric types
Dmitry Volodin's avatar
Dmitry Volodin committed
325
        self.labels = {self.get_labels_hash(m.metric, m.labels): m for m in metrics}
326
327
328
        for m in metrics:
            self.metric_configs[m.metric] += [m]
        # Process metrics collection
Dmitry Volodin's avatar
Dmitry Volodin committed
329
        persistent = set()
330
331
332
333
334
335
336
337
338
        for m in metrics:
            if m.id in self.seen_ids:
                self.logger.debug("[%s] Metric type is already collected. Skipping", m.metric)
                continue  # Already collected
            if m.metric not in self._mt_map:
                self.logger.debug("[%s] Metric type is not supported. Skipping", m.metric)
                continue
            # Call handlers
            for h in self.iter_handlers(m.metric):
Dmitry Volodin's avatar
Fix    
Dmitry Volodin committed
339
                hid = id(h)
340
                if not h.mt_volatile and hid in persistent:
Dmitry Volodin's avatar
Dmitry Volodin committed
341
                    continue  # persistent function already called
342
                h(self, self.metric_configs[m.metric])
343
                if not h.mt_volatile:
Dmitry Volodin's avatar
Fix    
Dmitry Volodin committed
344
                    persistent.add(hid)
345
346
347
348
349
350
                if m.id in self.seen_ids:
                    break  # Metric collected
        # Request snmp metrics from box
        if self.snmp_batch:
            self.collect_snmp_metrics()
        # Apply custom metric collection processes
351
        self.collect_profile_metrics(metrics)
Dmitry Volodin's avatar
Dmitry Volodin committed
352
353
        return self.get_metrics()

354
355
356
357
358
359
    def iter_handlers(self, metric):
        """
        Generator yilding possible handlers for metrics collection in order of preference
        :param metric: Metric type name
        :return: callable accepting *metrics*
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
360

361
        def is_applicable(f):
Dmitry Volodin's avatar
PEP8    
Dmitry Volodin committed
362
            if f.mt_has_script and f.mt_has_script not in self.scripts:
363
364
365
366
367
                return False
            if f.mt_has_capability and not self.has_capability(f.mt_has_capability):
                return False
            if f.mt_matcher and not getattr(self, f.mt_matcher, False):
                return False
368
369
            if f.mt_access == "S" and not self.has_snmp():
                return False
370
371
            return True

372
373
374
375
376
377
        pref = self.get_access_preference()
        handlers = self._mt_map[metric]
        pri = pref[0]
        sec = pref[1] if len(pref) > 1 else None
        # Iterate primary method
        for h in handlers:
378
            if (not h.mt_access or h.mt_access == pri) and is_applicable(h):
379
380
381
382
                yield h
        # Iterate secondary method
        if sec:
            for h in handlers:
383
                if h.mt_access == sec and is_applicable(h):
384
385
                    yield h

Dmitry Volodin's avatar
Dmitry Volodin committed
386
387
    def collect_profile_metrics(self, metrics):
        """
388
        Profile extension for very custom logic
Dmitry Volodin's avatar
Dmitry Volodin committed
389
        """
390
        # pylint: disable=unnecessary-pass
Dmitry Volodin's avatar
Dmitry Volodin committed
391
392
        pass

393
    def schedule_snmp_oids(self, rule, metric, metrics):
Dmitry Volodin's avatar
Dmitry Volodin committed
394
        """
395
396
397
398
399
400
401
        Schedule SNMP oid collection for given metrics.
        Used as partial function to build @metrics handler
        for JSON snmp rules

        :param rule: OIDRule instance
        :param metrics: List of MetricConfig instances
        :return:
Dmitry Volodin's avatar
Dmitry Volodin committed
402
        """
403
        for m in self.metric_configs[metric]:
Dmitry Volodin's avatar
Dmitry Volodin committed
404
            for oid, vtype, scale, labels in rule.iter_oids(self, m):
Dmitry Volodin's avatar
Dmitry Volodin committed
405
                self.snmp_batch[oid] += [
Dmitry Volodin's avatar
Dmitry Volodin committed
406
                    BatchConfig(id=m.id, metric=m.metric, labels=labels, type=vtype, scale=scale)
Dmitry Volodin's avatar
Dmitry Volodin committed
407
                ]
408
409
410
411
412
413
414
                # Mark as seen to stop further processing
                self.seen_ids.add(m.id)

    def collect_snmp_metrics(self):
        """
        Collect all scheduled SNMP metrics
        """
415
        # Run snmp batch
416
        if not self.snmp_batch:
417
418
            self.logger.debug("Nothing to fetch via SNMP")
            return
419
        # Build list of oids
420
        oids = set()
421
        for o in self.snmp_batch:
422
            if isinstance(o, str):
423
424
425
426
                oids.add(o)
            else:
                oids.update(o)
        oids = list(oids)
427
428
429
        results = self.snmp.get_chunked(
            oids=oids,
            chunk_size=self.get_snmp_metrics_get_chunk(),
Dmitry Volodin's avatar
Dmitry Volodin committed
430
            timeout_limits=self.get_snmp_metrics_get_timeout(),
431
        )
432
        # Process results
433
        for oid in self.snmp_batch:
434
            ts = self.get_ts()
435
            for batch in self.snmp_batch[oid]:
436
                if isinstance(oid, str):
437
438
439
                    if oid in results:
                        v = results[oid]
                        if v is None:
440
                            break
441
                    else:
Dmitry Volodin's avatar
Dmitry Volodin committed
442
                        self.logger.error("Failed to get SNMP OID %s", oid)
443
                        break
444
445
446
447
448
449
450
451
452
453
454
                elif callable(batch.scale):
                    # Multiple oids and calculated value
                    v = []
                    for o in oid:
                        if o in results:
                            vv = results[o]
                            if vv is None:
                                break
                            else:
                                v += [vv]
                        else:
Dmitry Volodin's avatar
Dmitry Volodin committed
455
                            self.logger.error("Failed to get SNMP OID %s", o)
456
457
458
459
                            break
                    # Check result does not contain None
                    if len(v) < len(oid):
                        self.logger.error(
Dmitry Volodin's avatar
Dmitry Volodin committed
460
461
462
                            "Cannot calculate complex value for %s " "due to missed values: %s",
                            oid,
                            v,
463
464
465
                        )
                        continue
                else:
466
                    self.logger.error(
Dmitry Volodin's avatar
Dmitry Volodin committed
467
                        "Cannot evaluate complex oid %s. " "Scale must be callable", oid
468
469
                    )
                    continue
470
471
472
473
474
475
                bv = batch
                self.set_metric(
                    id=bv.id,
                    metric=bv.metric,
                    value=v,
                    ts=ts,
Dmitry Volodin's avatar
Dmitry Volodin committed
476
                    labels=bv.labels,
477
                    type=bv.type,
Dmitry Volodin's avatar
Dmitry Volodin committed
478
                    scale=bv.scale,
479
                )
Dmitry Volodin's avatar
Dmitry Volodin committed
480
481
482
483

    def get_ifindex(self, name):
        return self.ifindexes.get(name)

484
    def get_ts(self) -> int:
Dmitry Volodin's avatar
Dmitry Volodin committed
485
486
487
        """
        Returns current timestamp in nanoseconds
        """
488
489
490
        if not self.ts:
            self.ts = int(time.time() * NS)
        return self.ts
Dmitry Volodin's avatar
Dmitry Volodin committed
491

Dmitry Volodin's avatar
Dmitry Volodin committed
492
    def set_metric(
493
494
495
496
497
498
499
500
501
        self,
        id: Union[int, Tuple[str, None]],
        metric: str = None,
        value: Union[int, float] = 0,
        ts: Optional[int] = None,
        labels: Optional[Union[List[str], Tuple[str]]] = None,
        type: str = "gauge",
        scale: Union[float, int, Callable] = 1,
        multi: bool = False,
Dmitry Volodin's avatar
Dmitry Volodin committed
502
    ):
Dmitry Volodin's avatar
Dmitry Volodin committed
503
504
        """
        Append metric to output
505
506
        :param id:
            Opaque id, as in request.
Dmitry Volodin's avatar
Dmitry Volodin committed
507
508
            May be tuple of (metric, labels), then it will be resolved automatically
            and *metric* and *labels* parameters may be ommited
509
510
511
512
        :param metric: Metric type as string.
            When None, try to get metric type from id tuple
        :param value: Measured value
        :param ts: Timestamp (nanoseconds precision)
Dmitry Volodin's avatar
Dmitry Volodin committed
513
        :param labels: labels. Either as requested, or refined.
514
515
516
517
518
519
520
521
522
            When None, try to get from id tuple
        :param type:
            Measure type. Possible values:
            "gauge"
            "counter"
            "delta"
            "bool"
        :param scale: Metric scale (Multiplier to be applied after all processing).
            When callable, function will be called, passing value as positional argument
Dmitry Volodin's avatar
Dmitry Volodin committed
523
524
        :param multi: True if single request can return several different labels.
            When False - only first call with composite labels for same labels will be returned
Dmitry Volodin's avatar
Dmitry Volodin committed
525
        """
526
527
528
529
        if value == SNMP_OVERLOAD_VALUE:
            self.logger.debug("SNMP Counter is full. Skipping value...")
            noc_metrics["error", ("type", "snmp_overload_drops")] += 1
            return
Dmitry Volodin's avatar
Dmitry Volodin committed
530
        if callable(scale):
531
532
533
            if not isinstance(value, list):
                value = [value]
            value = scale(*value)
Dmitry Volodin's avatar
Dmitry Volodin committed
534
            scale = 1
535
        if isinstance(id, tuple):
Dmitry Volodin's avatar
Dmitry Volodin committed
536
            # Composite id, extract type and labels and resolve
537
538
            if not metric:
                metric = id[0]
Dmitry Volodin's avatar
Dmitry Volodin committed
539
540
541
            if not labels:
                labels = id[1]
            mc = self.labels.get(self.get_labels_hash(*id))
Dmitry Volodin's avatar
Dmitry Volodin committed
542
            if not mc:
543
                # Not requested, ignoring
544
                self.logger.info("Not requesting, ignoring")
545
                return
Dmitry Volodin's avatar
Dmitry Volodin committed
546
            id = mc.id
547
548
            if not multi and id in self.seen_ids:
                return  # Already seen
Dmitry Volodin's avatar
Dmitry Volodin committed
549
550
551
552
553
        self.metrics += [
            {
                "id": id,
                "ts": ts or self.get_ts(),
                "metric": metric,
Dmitry Volodin's avatar
Dmitry Volodin committed
554
                "labels": labels or [],
Dmitry Volodin's avatar
Dmitry Volodin committed
555
556
557
558
559
                "value": value,
                "type": type,
                "scale": scale,
            }
        ]
560
        self.seen_ids.add(id)
Dmitry Volodin's avatar
Dmitry Volodin committed
561
562
563

    def get_metrics(self):
        return self.metrics
Dmitry Volodin's avatar
Dmitry Volodin committed
564
565

    @classmethod
566
    def get_oid_rule(cls, name):
Dmitry Volodin's avatar
Dmitry Volodin committed
567
        """
568
569
570
        Returns OIDRule type by its name
        :param name: oid rule type name
        :return: OIDRule descendant or None
Dmitry Volodin's avatar
Dmitry Volodin committed
571
        """
572
        return cls._oid_rules.get(name)
573

574
575
576
577
578
579
580
    @metrics(
        ["Interface | Last Сhange"],
        volatile=False,
        access="S",
    )
    def get_interface_lastchange(self, metrics):
        uptime = self.snmp.get("1.3.6.1.2.1.1.3.0")
581
        oids = {mib["IF-MIB::ifLastChange", str(m.ifindex)]: m for m in metrics if m.ifindex}
582
583
584
585
586
587
588
589
590
591
592
593
594
        result = self.snmp.get_chunked(
            oids=list(oids),
            chunk_size=self.get_snmp_metrics_get_chunk(),
            timeout_limits=self.get_snmp_metrics_get_timeout(),
        )
        ts = self.get_ts()
        for r in result:
            mc = oids[r]
            self.set_metric(
                id=mc.id,
                metric=mc.metric,
                value=(int(int(uptime - result[r]) / 8640000)),
                ts=ts,
Dmitry Volodin's avatar
Dmitry Volodin committed
595
                labels=mc.labels,
596
597
            )

598
599
    SENSOR_OID_SCALE: Dict[str, Union[int, Callable]] = {}  # oid -> scale

600
601
602
603
604
    @metrics(
        ["Sensor | Value"],
        access="S",
        volatile=False,
    )
605
    def collect_sensor_metrics(self, metrics: List[MetricConfig]):
606
607
        for m in metrics:
            if m.oid:
608
609
610
611
612
613
614
                try:
                    value = self.snmp.get(m.oid)
                    self.set_metric(
                        id=m.id,
                        metric=m.metric,
                        labels=m.labels,
                        value=float(value),
615
                        scale=self.SENSOR_OID_SCALE.get(m.oid, 1),
616
617
618
                    )
                except Exception:
                    continue
619

620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
    # @metrics(
    #     [
    #         "Interface | DOM | RxPower",
    #         "Interface | DOM | Temperature",
    #         "Interface | DOM | TxPower",
    #         "Interface | DOM | Voltage",
    #     ],
    #     has_capability="DB | Interfaces",
    #     has_script="get_dom_status",
    #     access="C",  # CLI version
    #     volatile=False,
    # )
    # def collect_dom_metrics(self, metrics):
    #     r = {}
    #     for m in self.scripts.get_dom_status():
    #         ipath = ["", "", "", m["interface"]]
    #         if m.get("temp_c") is not None:
    #             self.set_metric(id=("Interface | DOM | Temperature", ipath), value=m["temp_c"])
    #         if m.get("voltage_v") is not None:
    #             self.set_metric(id=("Interface | DOM | Voltage", ipath), value=m["voltage_v"])
    #         if m.get("optical_rx_dbm") is not None:
    #             self.set_metric(id=("Interface | DOM | RxPower", ipath), value=m["optical_rx_dbm"])
    #         if m.get("current_ma") is not None:
    #             self.set_metric(id=("Interface | DOM | Bias Current", ipath), value=m["current_ma"])
    #         if m.get("optical_tx_dbm") is not None:
    #             self.set_metric(id=("Interface | DOM | TxPower", ipath), value=m["optical_tx_dbm"])
    #     return r