profile.py 18.1 KB
Newer Older
Dmitry Volodin's avatar
Dmitry Volodin committed
1
# -*- coding: utf-8 -*-
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
2
3
4
5
# ---------------------------------------------------------------------
# Vendor: Huawei
# OS:     VRP
# ---------------------------------------------------------------------
Dmitry Volodin's avatar
Dmitry Volodin committed
6
# Copyright (C) 2007-2019 The NOC Project
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
7
8
# See LICENSE for details
# ---------------------------------------------------------------------
9

10
# Python modules
11
import re
12
import numpy as np
13
from itertools import dropwhile
14
from collections import defaultdict
Dmitry Volodin's avatar
Dmitry Volodin committed
15

Dmitry Volodin's avatar
Dmitry Volodin committed
16
# Third-party modules
17
from six.moves import zip_longest, zip
Dmitry Volodin's avatar
Dmitry Volodin committed
18

19
20
# NOC modules
from noc.core.profile.base import BaseProfile
Dmitry Volodin's avatar
Dmitry Volodin committed
21

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
22

Dmitry Volodin's avatar
Dmitry Volodin committed
23
class Profile(BaseProfile):
Dmitry Volodin's avatar
Dmitry Volodin committed
24
    name = "Huawei.VRP"
25
    pattern_more = [
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
26
        (r"^  ---- More ----\s*", " "),
27
28
        (r"[Cc]ontinue?\S+", "y\n\r"),
        (r"[Cc]onfirm?\S+", "y\n\r"),
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
29
30
        (r" [Aa]re you sure?\S+", "y\n\r"),
        (r"^Delete flash:", "y\n\r"),
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
31
        (r"^Squeeze flash:", "y\n\r"),
Dmitry Volodin's avatar
Dmitry Volodin committed
32
        (r"^The password needs to be changed\. Change now\? \[Y\/N\]\:", "n\n\r"),
33
    ]
Dmitry Volodin's avatar
Dmitry Volodin committed
34
35
    pattern_prompt = (
        r"^[<#\[](~|\*|)(?P<hostname>[a-zA-Z0-9-_\\\.\[\(/`'\"\|\s:,=]+)"
36
        r"(?:-[a-zA-Z0-9/\_]+)*[>#\]\)]"
Dmitry Volodin's avatar
Dmitry Volodin committed
37
38
39
40
41
42
43
44
45
    )
    pattern_syntax_error = (
        r"(ERROR: |% Wrong parameter found at|"
        r"% Unrecognized command found at|"
        r"Error:Too many parameters found|"
        r"% Too many parameters found at|"
        r"% Ambiguous command found at|"
        r"Error:\s*Unrecognized command found at|"
        r"Error:\s*Wrong parameter found at|"
46
        r"Error:\s*Incomplete command found at)"
Dmitry Volodin's avatar
Dmitry Volodin committed
47
    )
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
48

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
49
50
    command_more = " "
    config_volatile = ["^%.*?$"]
51
    command_disable_pager = "screen-length 0 temporary"
52
53
54
    command_enter_config = "system-view"
    command_leave_config = "return"
    command_save_config = "save"
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
55
    command_exit = "quit"
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
56
    rogue_chars = [re.compile(r"\x1b\[42D\s+\x1b\[42D"), "\r"]
57
58
    config_tokenizer = "indent"
    config_tokenizer_settings = {
59
        # "end_of_context": "#"
60
    }
61
    config_normalizer = "VRPNormalizer"
62
63
64
65
66
    confdb_defaults = [
        ("hints", "interfaces", "defaults", "admin-status", True),
        ("hints", "protocols", "lldp", "status", True),
        ("hints", "protocols", "spanning-tree", "status", True),
        ("hints", "protocols", "loop-detect", "status", False),
Dmitry Volodin's avatar
Dmitry Volodin committed
67
68
        ("hints", "protocols", "ntp", "mode", "server"),
        ("hints", "protocols", "ntp", "version", "3"),
69
    ]
Dmitry Volodin's avatar
Dmitry Volodin committed
70
    config_applicators = ["noc.core.confdb.applicator.collapsetagged.CollapseTaggedApplicator"]
71
    default_parser = "noc.cm.parsers.Huawei.VRP.base.BaseVRPParser"
72

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
73
    matchers = {
Dmitry Volodin's avatar
Dmitry Volodin committed
74
75
        "is_kernel_3": {"version": {"$gte": "3.0", "$lt": "5.0"}},
        "is_kernelgte_5": {"version": {"$gte": "5.0"}},
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
76
        "is_bad_platform": {
77
            "version": {"$regex": r"5.20.+"},
Dmitry Volodin's avatar
Dmitry Volodin committed
78
            "platform": {"$in": ["S5628F", "S5628F-HI"]},
79
        },
Dmitry Volodin's avatar
Dmitry Volodin committed
80
81
82
83
        "is_ne_platform": {"platform": {"$regex": "^NE"}},
        "is_ar": {"platform": {"$regex": r"^AR\d+.+"}},
        "is_extended_entity_mib_supported": {"caps": {"$in": ["Huawei | MIB | ENTITY-EXTENT-MIB"]}},
        "is_stack": {"caps": {"$in": ["Stack | Members"]}},
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
84
85
    }

Dmitry Volodin's avatar
Dmitry Volodin committed
86
87
88
    rx_ver = re.compile(
        r"((?:(\d)\.(\d+))\s*)?\(?(?:V(\d+)|)(?:R(\d+)|)(?:C(\d+)|)(?:B(\d+)|)(?:SPC(\d+)|)\)?"
    )
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
89
90
    rx_ver_kern = re.compile(r"\s*(\d)\.(\d+)\s*")
    rx_ver_rel = re.compile(r"\(?(?:V(\d+)|)(?:R(\d+)|)(?:C(\d+)|)(?:B(\d+)|)(?:SPC(\d+)|)\)?")
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

    def cmp_version(self, x, y):
        """
        Huawei VRP system software version is divided into "core version" (or "kernel") and "release" two.
        We are talking about is the VRP 1.x, 2.x, 3.x, And now VRP 5.x and 8.x versions.
        Huawei release status of the VRP system is based on V, R, C three letters
        (representing three different version number) were identified, the basic format is VxxxRxxxCxx:
        * Vxxx logo products / solutions change program main product platform, called V version number.
        * Rxxx identification for generic versions of all customers posting, known as the R version number.
        * version of C customized version of the fast to meet the development version of R different types
          based on customer's demand, known as the C version number.
        [Note] the above described V version and R version number, independent number, do not influence each other.
               It is between them and no affiliation. For example, the product can occur platform changes,
               and functional properties remain unchanged,
               such as the original VR version is V100R005, the new VR version V200R005.
          In the same R version, C version of XX from 00 to 1 units numbered. If the R version number change,
          the C version number of the XX began to re numbered 01, such as V100R001C01, V100R001C02, V100R002C01
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
        :param x: [12358].x (VxxxRxxxCxxBxxx)
        :param y: [12358].x (VxxxRxxxCxxBxxx)
        :return: <0 , if v1<v2
                  0 , if v1==v2
                 >0 , if v1>v2
               None , if v1 and v2 cannot be compared

        >>> Profile().cmp_version("5.30 (V100R005C02B236)", "5.30")
        0
        >>> Profile().cmp_version("5.30 (V100R005C02B236)", "5.40")
        -1
        >>> Profile().cmp_version("R006C02", "5.30 (V100R005C02B236)")
        1
        >>> Profile().cmp_version("5.30 (V100R003C00SPC100)", "5.30 (V100R005C02B236)")
        -1
        >>> Profile().cmp_version("5.80 (V100R005C02SPC100)", "5.80 (V100R005C02B236)")
        0
        >>> Profile().cmp_version("5.00 (V100R005C02SPC100)", "3.80 (V100R005C02B236)")
        1
        >>> Profile().cmp_version("3.10 (V100R005C02SPC100)", "3.80 (V100R005C02B236)")
        -1
        >>> Profile().cmp_version("100", "5.30 (V100R005C02B236)")
130
        """
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
131
132
133
        a, b = self.rx_ver.search(str(x)).groups()[1:], self.rx_ver.search(str(y)).groups()[1:]
        # if set(self.rx_ver.search(x).groups()) and self.rx_ver.search(y):
        if any(a) and any(b):
Dmitry Volodin's avatar
Dmitry Volodin committed
134
135
136
137
138
139
140
141
142
143
            r = list(
                dropwhile(
                    lambda s: s == 0,
                    [
                        (int(a) > int(b)) - (int(a) < int(b))
                        for a, b in zip(a, b)  # noqa
                        if a is not None and b is not None
                    ],
                )
            )
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
            return r[0] if r else 0
        else:
            return None

    INTERFACE_TYPES = {
        "Aux": "tunnel",
        "Cellular": "tunnel",
        "Eth-Trunk": "aggregated",
        "Ip-Trunk": "aggregated",
        "XGigabitEthernet": "physical",
        "Ten-GigabitEthernet": "physical",
        "GigabitEthernet": "physical",
        "FastEthernet": "physical",
        "Ethernet": "physical",
        "Cascade": "physical",
        "Logic-Channel": "tunnel",
        "LoopBack": "loopback",
        "MEth": "management",
        "M-Ethernet": "management",
        "MTunnel": None,
        "Ring-if": "physical",
        "Tunnel": "tunnel",
        "Virtual-Ethernet": None,
        "Virtual-Template": "template",
        "Bridge-Template": "template",
        "Bridge-template": "template",
        "Vlanif": "SVI",
        "Vlan-interface": "SVI",
        "NULL": "null",
        "RprPos": "unknown",
        "Rpr": "unknown",
Serg's avatar
Serg committed
175
        "10GE": "physical",
176
        "40GE": "physical",
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
177
178
        "100GE": "physical",
        "Serial": None,
Dmitry Volodin's avatar
Dmitry Volodin committed
179
        "Pos": None,
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
180
181
    }

182
183
    rx_iftype = re.compile(r"^(\D+?|\d{2,3}\S+?)\d+.*$")

Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
184
185
    @classmethod
    def get_interface_type(cls, name):
186
187
        if cls.rx_iftype.match(name):
            return cls.INTERFACE_TYPES.get(cls.rx_iftype.match(name).group(1))
Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
188
        return cls.INTERFACE_TYPES.get(name)
189

190
    def generate_prefix_list(self, name, pl, strict=True):
191
192
193
194
195
196
197
198
199
        me = "ip ip-prefix %s permit %%s" % name
        mne = "ip ip-prefix %s permit %%s le %%d" % name
        r = ["undo ip ip-prefix %s" % name]
        for prefix, min_len, max_len in pl:
            if min_len == max_len:
                r += [me % prefix]
            else:
                r += [mne % (prefix, max_len)]
        return "\n".join(r)
200

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
201
    rx_interface_name = re.compile(
Dmitry Volodin's avatar
Dmitry Volodin committed
202
203
        r"^(?P<type>XGE|Ten-GigabitEthernet|(?<!100)GE|Eth|MEth)" r"(?P<number>[\d/]+(\.\d+)?)$"
    )
204
205
206

    def convert_interface_name(self, s):
        """
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
207
208
        >>> Profile().convert_interface_name("XGE2/0/0")
        'XGigabitEthernet2/0/0'
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
209
210
        >>> Profile().convert_interface_name("Ten-GigabitEthernet2/0/0")
        'XGigabitEthernet2/0/0'
211
212
        >>> Profile().convert_interface_name("GE2/0/0")
        'GigabitEthernet2/0/0'
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
213
214
215
216
        >>> Profile().convert_interface_name("Eth2/0/0")
        'Ethernet2/0/0'
        >>> Profile().convert_interface_name("MEth2/0/0")
        'M-Ethernet2/0/0'
217
        """
218
        s = str(s)  # avoid `expected string or buffer` error
219
220
221
        match = self.rx_interface_name.match(s)
        if not match:
            return s
Dmitry Volodin's avatar
Dmitry Volodin committed
222
223
224
225
226
227
228
229
230
231
232
233
234
        return "%s%s" % (
            {
                "Loop": "LoopBack",
                "Ten-GigabitEthernet": "XGigabitEthernet",
                "XGE": "XGigabitEthernet",
                "GE": "GigabitEthernet",
                "Eth": "Ethernet",
                "MEth": "M-Ethernet",
                "VE": "Virtual-Ethernet"
                # "Vlanif": "Vlan-interface" - need testing
            }[match.group("type")],
            match.group("number"),
        )
235
236
237
238
239
240
241

    def convert_mac(self, mac):
        """
        Convert 00:11:22:33:44:55 style MAC-address to 0011-2233-4455
        """
        v = mac.replace(":", "").lower()
        return "%s-%s-%s" % (v[:4], v[4:8], v[8:])
242

Andrey Vertiprahov's avatar
Andrey Vertiprahov committed
243
    spaces_rx = re.compile(r"^\s{42}|^\s{16}", re.DOTALL | re.MULTILINE)
Aleksey Shirokih's avatar
Aleksey Shirokih committed
244

245
    def clean_spaces(self, config):
Aleksey Shirokih's avatar
Aleksey Shirokih committed
246
        config = self.spaces_rx.sub("", config)
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
247
        return config
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
248
249

    def fix_version(self, v):
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
250
251
252
        # CLI return S5628F-HI as platform, but SNMP return S5628F
        BAD_PLATFORMS = ["S5628F", "S5628F-HI"]
        if v["platform"] in BAD_PLATFORMS and v["version"] == "5.20":
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
253
            # Do not change these numbers. Used in get_switchport script
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
254
            v["version"] = "3.10"
Dmitry Lukhtionov's avatar
Fix    
Dmitry Lukhtionov committed
255
        return v["version"]
256
257

    @staticmethod
258
259
260
261
262
263
264
265
    def parse_table(block, part_name=None):
        """
        :param part_name: If not None - part name not on block
        :type part_name: str
        :param block: Block with table
        :type block: str
        :return:
        """
266
        # @todo migrate to parse_block
Dmitry Volodin's avatar
Dmitry Volodin committed
267
268
        k_v_splitter = re.compile(r"\s*(?P<key>.+?):\s+(?P<value>.+?)(?:\s\s|\n)", re.IGNORECASE)
        part_splitter = re.compile(r"\s*(?P<part_name>\S+?):\s*\n", re.IGNORECASE)
269
        r = {}
270
        is_table = False
271
272
273
274
275
        is_part = False
        k_v_list = []
        row = []
        if part_name:
            is_part = True
276
        for line in block.splitlines(True):
277
278
            # print l
            # Part section
279
            if "-" * 5 in line:
280
                is_table = True
281
                # print("Table start")
282
                continue
283
            if part_splitter.match(line) and is_part:
284
285
286
287
288
                # @todo many table in part ?
                is_part = False
                is_table = False
                r[part_name] = dict(k_v_list)
                r[part_name]["table"] = row
289
290
                # print("Key-Val: %s" % k_v_list)
                # print("Part End")
291
292
                k_v_list = []
                row = []
293
            if part_splitter.match(line) and not is_part:
294
                is_part = True
295
                part_name = part_splitter.match(line).group(1)
296
297
                # print("Part start: %s" % part_name)
                continue
298
            if not line.strip():
Dmitry Lukhtionov's avatar
Fix    
Dmitry Lukhtionov committed
299
                # is_part = False
300
                # print("Part End")
301
                continue
302
303
            # Parse Section
            if is_part and is_table:
304
                row.append(line.split())
305
            elif is_part and not is_table:
306
                k_v_list.extend(k_v_splitter.findall(line))
307
308
309
310
311
312
313
314
            continue
        else:
            r[part_name] = dict(k_v_list)
            if row:
                r[part_name]["table"] = row
            # r[part_name] = dict(k_v_list)
            # r[part_name]["table"] = row
        return r
315
316
317
318
319
320
321
322
323

    @staticmethod
    def parse_ifaces(e=""):
        # Parse display interfaces output command for Huawei
        r = defaultdict(dict)
        current_iface = ""
        for line in e.splitlines():
            if not line:
                continue
Dmitry Volodin's avatar
Dmitry Volodin committed
324
325
326
327
328
329
330
331
332
            if (
                line.startswith("LoopBack")
                or line.startswith("MEth")
                or line.startswith("Ethernet")
                or line.startswith("GigabitEthernet")
                or line.startswith("XGigabitEthernet")
                or line.startswith("Vlanif")
                or line.startswith("NULL")
            ):
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
                current_iface = line.split()[0]
                continue
            # k, v count
            split = line.count(":") + line.count(" is ") + line.count(" rate ")
            if "Switch Port" in line:
                line = line[12:]
            elif "Route Port" in line:
                line = line[11:]
            # while split:
            for part in line.split(",", split - 1):
                if ":" in part:
                    k, v = part.split(":", 1)
                elif " is " in part:
                    k, v = part.split("is", 1)
                elif " rate " in part:
                    k, v = part.split("rate", 1)
                    k = k + "rate"
                else:
                    continue
                r[current_iface][k.strip()] = v.strip()
        return r
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377

    @staticmethod
    def update_dict(s, d):
        for k in d:
            if k in s:
                s[k] += d[k]
            else:
                s[k] = d[k]

    @staticmethod
    def parse_header(v):
        """
        Parse header structured multiline format:
        Config    Current Agg     Min    Ld Share  Flags Ld Share  Agg Link  Link Up
        Master    Master  Control Active Algorithm       Group     Mbr State Transitions
        :param v:
        :return: Dictionary {start column position: header}
        {10: 'Config Master', 18: 'Current Master', 26: 'Agg Control', 33: 'Min Active',
         43: 'Ld Share Algorithm', 49: 'Flags ', 59: 'Ld Share Group', 63: 'Agg Mbr', 69: 'Link State'}
        """
        head = []
        empty_header = None
        header = {}

Dmitry Volodin's avatar
Dmitry Volodin committed
378
        for num, lines in enumerate(zip_longest(*v, fillvalue="-")):
379
380
            #
            if empty_header is None:
Dmitry Volodin's avatar
Dmitry Volodin committed
381
                empty_header = (" ",) * len(lines)
382
383
                head += [lines]
                continue
Dmitry Volodin's avatar
Dmitry Volodin committed
384
            if set(head[-1]) == {" "} and lines != empty_header:
385
386
387
388
389
                head = np.array(head)
                # Transpone list header string
                header[num] = " ".join(["".join(s).strip() for s in head.transpose().tolist()])
                head = []
            head += [lines]
390
391
392
393
        else:
            # last column
            head = np.array(head)
            header[num] = " ".join(["".join(s).strip(" -") for s in head.transpose().tolist()])
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

        return header

    def parse_block(self, block):
        """
        Block1:
        Local:
        LAG ID: 8                   WorkingMode: LACP
        Preempt Delay: Disabled     Hash arithmetic: According to SIP-XOR-DIP
        System Priority: 32768      System ID: 5489-9875-1457
        Least Active-linknumber: 1  Max Active-linknumber: 8
        Operate status: up          Number Of Up Port In Trunk: 2
        --------------------------------------------------------------------------------
        ActorPortName          Status   PortType PortPri PortNo PortKey PortState Weight
        XGigabitEthernet10/0/4 Selected 10GE     32768   95     2113    10111100  1
        XGigabitEthernet10/0/5 Selected 10GE     32768   96     2113    10111100  1

        Block2:
        Partner:
        --------------------------------------------------------------------------------
        ActorPortName          SysPri   SystemID        PortPri PortNo PortKey PortState
        XGigabitEthernet10/0/4 65535    0011-bbbb-ddda  255     1      33      10111100
        XGigabitEthernet10/0/5 65535    0011-bbbb-ddda  255     2      33      10111100

        :param block:
        :return:
        """

        r = defaultdict(dict)
        part_name = ""
Dmitry Volodin's avatar
Dmitry Volodin committed
424
425
        k_v_splitter = re.compile(r"\s*(?P<key>.+?):\s+(?P<value>.+?)(?:\s\s|\n)", re.IGNORECASE)
        part_splitter = re.compile(r"\s*(?P<part_name>\S+?):\s*\n", re.IGNORECASE)
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472

        # r = {}
        is_table = False  # Table block, start after -----
        is_table_header = False  # table header first line after ----
        k_v_list = []
        ph = {}
        for line in block.splitlines(True):
            # print l
            # Part section
            if "-" * 5 in line:
                # -- - starting table and end key-value lines
                is_table = True
                is_table_header = True
                r[part_name]["table"] = []
                if k_v_list:
                    r[part_name].update(dict(k_v_list))
                    k_v_list = []
                # print("Table start")
                continue
            if is_table_header:
                # Parse table header for detect column border
                # If needed more than one line - needed count
                ph = self.parse_header([line])
                is_table_header = False
                continue
            if part_splitter.match(line):
                # Part spliter (Local:\n, Partner:\n)
                # or (is_table and not line.strip())
                # @todo many table in part ?
                is_table = False
                ph = {}
                part_name = part_splitter.match(line).group(1)
                continue
            if ":" in line and not is_table:
                # Key-value block
                k_v_list.extend(k_v_splitter.findall(line))
            elif ph and is_table:
                # parse table row
                i = 0
                field = {}
                for num in sorted(ph):
                    # Shift column border
                    left = i
                    right = num
                    v = line[left:right].strip()
                    field[ph[num]] = [v] if v else []
                    i = num
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
473
                if not field[ph[min(ph)]] and r[part_name]["table"]:
474
475
476
477
                    self.update_dict(r[part_name]["table"][-1], field)
                else:
                    r[part_name]["table"] += [field]
        return r