CSIT-1337: Migrate L2Util library from VAT to PAPI
[csit.git] / resources / libraries / python / L2Util.py
1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """L2 Utilities Library."""
15
16 import binascii
17 from textwrap import wrap
18
19 from enum import IntEnum
20
21 from resources.libraries.python.PapiExecutor import PapiExecutor
22 from resources.libraries.python.topology import Topology
23 from resources.libraries.python.ssh import exec_cmd_no_error
24
25
26 class L2VtrOp(IntEnum):
27     """VLAN tag rewrite operation."""
28     L2_VTR_DISABLED = 0
29     L2_VTR_PUSH_1 = 1
30     L2_VTR_PUSH_2 = 2
31     L2_VTR_POP_1 = 3
32     L2_VTR_POP_2 = 4
33     L2_VTR_TRANSLATE_1_1 = 5
34     L2_VTR_TRANSLATE_1_2 = 6
35     L2_VTR_TRANSLATE_2_1 = 7
36     L2_VTR_TRANSLATE_2_2 = 8
37
38
39 class L2Util(object):
40     """Utilities for l2 configuration."""
41
42     @staticmethod
43     def mac_to_int(mac_str):
44         """Convert MAC address from string format (e.g. 01:02:03:04:05:06) to
45         integer representation (1108152157446).
46
47         :param mac_str: MAC address in string representation.
48         :type mac_str: str
49         :returns: Integer representation of MAC address.
50         :rtype: int
51         """
52         return int(mac_str.replace(':', ''), 16)
53
54     @staticmethod
55     def int_to_mac(mac_int):
56         """Convert MAC address from integer representation (e.g. 1108152157446)
57         to string format (01:02:03:04:05:06).
58
59         :param mac_int: MAC address in integer representation.
60         :type mac_int: int
61         :returns: String representation of MAC address.
62         :rtype: str
63         """
64         return ':'.join(wrap("{:012x}".format(mac_int), width=2))
65
66     @staticmethod
67     def mac_to_bin(mac_str):
68         """Convert MAC address from string format (e.g. 01:02:03:04:05:06) to
69         binary representation (\x01\x02\x03\x04\x05\x06).
70
71         :param mac_str: MAC address in string representation.
72         :type mac_str: str
73         :returns: Binary representation of MAC address.
74         :rtype: binary
75         """
76         return binascii.unhexlify(mac_str.replace(':', ''))
77
78     @staticmethod
79     def bin_to_mac(mac_bin):
80         """Convert MAC address from binary representation
81         (\x01\x02\x03\x04\x05\x06) to string format (e.g. 01:02:03:04:05:06).
82
83         :param mac_bin: MAC address in binary representation.
84         :type mac_bin: binary
85         :returns: String representation of MAC address.
86         :rtype: str
87         """
88         x = ':'.join(binascii.hexlify(mac_bin)[i:i + 2]
89                      for i in range(0, 12, 2))
90         return str(x.decode('ascii'))
91
92     @staticmethod
93     def vpp_add_l2fib_entry(node, mac, interface, bd_id, static_mac=1,
94                             filter_mac=0, bvi_mac=0):
95         """ Create a static L2FIB entry on a VPP node.
96
97         :param node: Node to add L2FIB entry on.
98         :param mac: Destination mac address in string format 01:02:03:04:05:06.
99         :param interface: Interface name or sw_if_index.
100         :param bd_id: Bridge domain index.
101         :param static_mac: Set to 1 to create static MAC entry.
102             (Default value = 1)
103         :param filter_mac: Set to 1 to drop packet that's source or destination
104             MAC address contains defined MAC address. (Default value = 0)
105         :param bvi_mac: Set to 1 to create entry that points to BVI interface.
106             (Default value = 0)
107         :type node: dict
108         :type mac: str
109         :type interface: str or int
110         :type bd_id: int or str
111         :type static_mac: int or str
112         :type filter_mac: int or str
113         :type bvi_mac: int or str
114         """
115
116         if isinstance(interface, basestring):
117             sw_if_index = Topology.get_interface_sw_index(node, interface)
118         else:
119             sw_if_index = interface
120
121         cmd = 'l2fib_add_del'
122         err_msg = 'Failed to add L2FIB entry on host {host}'.format(
123             host=node['host'])
124         args = dict(mac=L2Util.mac_to_bin(mac),
125                     bd_id=int(bd_id),
126                     sw_if_index=sw_if_index,
127                     is_add=1,
128                     static_mac=int(static_mac),
129                     filter_mac=int(filter_mac),
130                     bvi_mac=int(bvi_mac))
131         with PapiExecutor(node) as papi_exec:
132             papi_exec.add(cmd, **args).get_replies(err_msg).\
133                 verify_reply(err_msg=err_msg)
134
135     @staticmethod
136     def create_l2_bd(node, bd_id, flood=1, uu_flood=1, forward=1, learn=1,
137                      arp_term=0):
138         """Create an L2 bridge domain on a VPP node.
139
140         :param node: Node where we wish to crate the L2 bridge domain.
141         :param bd_id: Bridge domain index.
142         :param flood: Enable/disable bcast/mcast flooding in the BD.
143             (Default value = 1)
144         :param uu_flood: Enable/disable unknown unicast flood in the BD.
145             (Default value = 1)
146         :param forward: Enable/disable forwarding on all interfaces in
147             the BD. (Default value = 1)
148         :param learn: Enable/disable MAC learning on all interfaces in the BD.
149             (Default value = 1)
150         :param arp_term: Enable/disable arp termination in the BD.
151             (Default value = 1)
152         :type node: dict
153         :type bd_id: int or str
154         :type flood: int or str
155         :type uu_flood: int or str
156         :type forward: int or str
157         :type learn: int or str
158         :type arp_term: int or str
159         """
160
161         cmd = 'bridge_domain_add_del'
162         err_msg = 'Failed to create L2 bridge domain on host {host}'.format(
163             host=node['host'])
164         args = dict(bd_id=int(bd_id),
165                     flood=int(flood),
166                     uu_flood=int(uu_flood),
167                     forward=int(forward),
168                     learn=int(learn),
169                     arp_term=int(arp_term),
170                     is_add=1)
171         with PapiExecutor(node) as papi_exec:
172             papi_exec.add(cmd, **args).get_replies(err_msg).\
173                 verify_reply(err_msg=err_msg)
174
175     @staticmethod
176     def add_interface_to_l2_bd(node, interface, bd_id, shg=0, port_type=0):
177         """Add an interface to the L2 bridge domain.
178
179         Get SW IF ID and add it to the bridge domain.
180
181         :param node: Node where we want to execute the command that does this.
182         :param interface: Interface name.
183         :param bd_id: Bridge domain index.
184         :param shg: Split-horizon group index. (Default value = 0)
185         :param port_type: Port mode: 0 - normal, 1 - BVI, 2 - UU_FWD.
186             (Default value = 0)
187         :type node: dict
188         :type interface: str
189         :type bd_id: int or str
190         :type shg: int or str
191         :type port_type: int or str
192         """
193
194         sw_if_index = Topology.get_interface_sw_index(node, interface)
195
196         cmd = 'sw_interface_set_l2_bridge'
197         err_msg = 'Failed to add interface {ifc} to L2 bridge domain on host ' \
198                   '{host}'.format(ifc=interface, host=node['host'])
199         args = dict(rx_sw_if_index=sw_if_index,
200                     bd_id=int(bd_id),
201                     shg=int(shg),
202                     port_type=int(port_type),
203                     enable=1)
204         with PapiExecutor(node) as papi_exec:
205             papi_exec.add(cmd, **args).get_replies(err_msg).\
206                 verify_reply(err_msg=err_msg)
207
208     @staticmethod
209     def vpp_add_l2_bridge_domain(node, bd_id, port_1, port_2, learn=True):
210         """Add L2 bridge domain with 2 interfaces to the VPP node.
211
212         :param node: Node to add L2BD on.
213         :param bd_id: Bridge domain ID.
214         :param port_1: First interface name added to L2BD.
215         :param port_2: Second interface name added to L2BD.
216         :param learn: Enable/disable MAC learn.
217         :type node: dict
218         :type bd_id: int
219         :type port_1: str
220         :type port_2: str
221         :type learn: bool
222         """
223
224         sw_if_index1 = Topology.get_interface_sw_index(node, port_1)
225         sw_if_index2 = Topology.get_interface_sw_index(node, port_2)
226         learn_int = 1 if learn else 0
227
228         cmd1 = 'bridge_domain_add_del'
229         args1 = dict(bd_id=int(bd_id),
230                      flood=1,
231                      uu_flood=1,
232                      forward=1,
233                      learn=learn_int,
234                      arp_term=0,
235                      is_add=1)
236
237         cmd2 = 'sw_interface_set_l2_bridge'
238         args2 = dict(rx_sw_if_index=sw_if_index1,
239                      bd_id=int(bd_id),
240                      shg=0,
241                      port_type=0,
242                      enable=1)
243
244         args3 = dict(rx_sw_if_index=sw_if_index2,
245                      bd_id=int(bd_id),
246                      shg=0,
247                      port_type=0,
248                      enable=1)
249
250         err_msg = 'Failed to add L2 bridge domain with 2 interfaces on host' \
251                   ' {host}'.format(host=node['host'])
252
253         with PapiExecutor(node) as papi_exec:
254             papi_exec.add(cmd1, **args1).add(cmd2, **args2).add(cmd2, **args3).\
255                 get_replies(err_msg).verify_replies(err_msg=err_msg)
256
257     @staticmethod
258     def vpp_setup_bidirectional_cross_connect(node, interface1, interface2):
259         """Create bidirectional cross-connect between 2 interfaces on vpp node.
260
261         :param node: Node to add bidirectional cross-connect.
262         :param interface1: First interface name or sw_if_index.
263         :param interface2: Second interface name or sw_if_index.
264         :type node: dict
265         :type interface1: str or int
266         :type interface2: str or int
267         """
268
269         if isinstance(interface1, basestring):
270             sw_iface1 = Topology().get_interface_sw_index(node, interface1)
271         else:
272             sw_iface1 = interface1
273
274         if isinstance(interface2, basestring):
275             sw_iface2 = Topology().get_interface_sw_index(node, interface2)
276         else:
277             sw_iface2 = interface2
278
279         cmd = 'sw_interface_set_l2_xconnect'
280         args1 = dict(rx_sw_if_index=sw_iface1,
281                      tx_sw_if_index=sw_iface2,
282                      enable=1)
283         args2 = dict(rx_sw_if_index=sw_iface2,
284                      tx_sw_if_index=sw_iface1,
285                      enable=1)
286
287         err_msg = 'Failed to add L2 cross-connect between two interfaces on' \
288                   ' host {host}'.format(host=node['host'])
289
290         with PapiExecutor(node) as papi_exec:
291             papi_exec.add(cmd, **args1).add(cmd, **args2).get_replies(err_msg).\
292                 verify_replies(err_msg=err_msg)
293
294     @staticmethod
295     def vpp_setup_bidirectional_l2_patch(node, interface1, interface2):
296         """Create bidirectional l2 patch between 2 interfaces on vpp node.
297
298         :param node: Node to add bidirectional l2 patch.
299         :param interface1: First interface name or sw_if_index.
300         :param interface2: Second interface name or sw_if_index.
301         :type node: dict
302         :type interface1: str or int
303         :type interface2: str or int
304         """
305
306         if isinstance(interface1, basestring):
307             sw_iface1 = Topology().get_interface_sw_index(node, interface1)
308         else:
309             sw_iface1 = interface1
310
311         if isinstance(interface2, basestring):
312             sw_iface2 = Topology().get_interface_sw_index(node, interface2)
313         else:
314             sw_iface2 = interface2
315
316         cmd = 'l2_patch_add_del'
317         args1 = dict(rx_sw_if_index=sw_iface1,
318                      tx_sw_if_index=sw_iface2,
319                      is_add=1)
320         args2 = dict(rx_sw_if_index=sw_iface2,
321                      tx_sw_if_index=sw_iface1,
322                      is_add=1)
323
324         err_msg = 'Failed to add L2 patch between two interfaces on' \
325                   ' host {host}'.format(host=node['host'])
326
327         with PapiExecutor(node) as papi_exec:
328             papi_exec.add(cmd, **args1).add(cmd, **args2).get_replies(err_msg).\
329                 verify_replies(err_msg=err_msg)
330
331     @staticmethod
332     def linux_add_bridge(node, br_name, if_1, if_2, set_up=True):
333         """Bridge two interfaces on linux node.
334
335         :param node: Node to add bridge on.
336         :param br_name: Bridge name.
337         :param if_1: First interface to be added to the bridge.
338         :param if_2: Second interface to be added to the bridge.
339         :param set_up: Change bridge interface state to up after create bridge.
340             Optional. Default: True.
341         :type node: dict
342         :type br_name: str
343         :type if_1: str
344         :type if_2: str
345         :type set_up: bool
346         """
347
348         cmd = 'brctl addbr {0}'.format(br_name)
349         exec_cmd_no_error(node, cmd, sudo=True)
350         cmd = 'brctl addif {0} {1}'.format(br_name, if_1)
351         exec_cmd_no_error(node, cmd, sudo=True)
352         cmd = 'brctl addif {0} {1}'.format(br_name, if_2)
353         exec_cmd_no_error(node, cmd, sudo=True)
354         if set_up:
355             cmd = 'ip link set dev {0} up'.format(br_name)
356             exec_cmd_no_error(node, cmd, sudo=True)
357
358     @staticmethod
359     def linux_del_bridge(node, br_name, set_down=True):
360         """Delete bridge from linux node.
361
362         ..note:: The network interface corresponding to the bridge must be
363             down before it can be deleted!
364
365         :param node: Node to delete bridge from.
366         :param br_name: Bridge name.
367         :param set_down: Change bridge interface state to down before delbr
368             command. Optional. Default: True.
369         :type node: dict
370         :type br_name: str
371         :type set_down: bool
372         """
373
374         if set_down:
375             cmd = 'ip link set dev {0} down'.format(br_name)
376             exec_cmd_no_error(node, cmd, sudo=True)
377         cmd = 'brctl delbr {0}'.format(br_name)
378         exec_cmd_no_error(node, cmd, sudo=True)
379
380     @staticmethod
381     def vpp_get_bridge_domain_data(node, bd_id=0xffffffff):
382         """Get all bridge domain data from a VPP node. If a domain ID number is
383         provided, return only data for the matching bridge domain.
384
385         :param node: VPP node to get bridge domain data from.
386         :param bd_id: Numeric ID of a specific bridge domain.
387         :type node: dict
388         :type bd_id: int
389         :returns: List of dictionaries containing data for each bridge domain,
390             or a single dictionary for the specified bridge domain.
391         :rtype: list or dict
392         """
393
394         # TODO: set following variable per whole suite when planned FIB API
395         # changes are merged in VPP
396         bitwise_non_zero = 0xffffffff   # equals to ~0 used in vpp code
397
398         cmd = 'bridge_domain_dump'
399         cmd_reply = 'bridge_domain_details'
400         args = dict(bd_id=bd_id if isinstance(bd_id, int) else int(bd_id))
401         err_msg = 'Failed to get L2FIB dump on host {host}'.format(
402             host=node['host'])
403         with PapiExecutor(node) as papi_exec:
404             papi_resp = papi_exec.add(cmd, **args).get_dump(err_msg)
405
406         data = papi_resp.reply[0]['api_reply']
407
408         bd_data = list() if bd_id == bitwise_non_zero else dict()
409         for bridge_domain in data:
410             if bd_id == bitwise_non_zero:
411                 bd_data.append(bridge_domain[cmd_reply])
412             else:
413                 if bridge_domain[cmd_reply]['bd_id'] == bd_id:
414                     return bridge_domain[cmd_reply]
415
416         return bd_data
417
418     @staticmethod
419     def l2_vlan_tag_rewrite(node, interface, tag_rewrite_method,
420                             push_dot1q=True, tag1_id=None, tag2_id=None):
421         """Rewrite tags in ethernet frame.
422
423         :param node: Node to rewrite tags.
424         :param interface: Interface on which rewrite tags.
425         :param tag_rewrite_method: Method of tag rewrite.
426         :param push_dot1q: Optional parameter to disable to push dot1q tag
427             instead of dot1ad.
428         :param tag1_id: Optional tag1 ID for VLAN.
429         :param tag2_id: Optional tag2 ID for VLAN.
430         :type node: dict
431         :type interface: str or int
432         :type tag_rewrite_method: str
433         :type push_dot1q: bool
434         :type tag1_id: int
435         :type tag2_id: int
436         """
437
438         tag1_id = int(tag1_id) if tag1_id else 0
439         tag2_id = int(tag2_id) if tag2_id else 0
440
441         vtr_oper = getattr(L2VtrOp, 'L2_VTR_{}'.format(
442             tag_rewrite_method.replace('-', '_').upper()))
443
444         if isinstance(interface, basestring):
445             iface_key = Topology.get_interface_by_name(node, interface)
446             sw_if_index = Topology.get_interface_sw_index(node, iface_key)
447         else:
448             sw_if_index = interface
449
450         cmd = 'l2_interface_vlan_tag_rewrite'
451         args = dict(sw_if_index=sw_if_index,
452                     vtr_op=int(vtr_oper),
453                     push_dot1q=int(push_dot1q),
454                     tag1=tag1_id,
455                     tag2=tag2_id)
456         err_msg = 'Failed to set VLAN TAG rewrite on host {host}'.format(
457             host=node['host'])
458         with PapiExecutor(node) as papi_exec:
459             papi_exec.add(cmd, **args).get_replies(err_msg).\
460                 verify_reply(err_msg=err_msg)
461
462     @staticmethod
463     def get_l2_fib_table(node, bd_id):
464         """Retrieves the L2 FIB table.
465
466         :param node: VPP node.
467         :param bd_id: Index of the bridge domain.
468         :type node: dict
469         :type bd_id: int
470         :returns: L2 FIB table.
471         :rtype: list
472         """
473
474         cmd = 'l2_fib_table_dump'
475         cmd_reply = 'l2_fib_table_details'
476         args = dict(bd_id=int(bd_id))
477         err_msg = 'Failed to get L2FIB dump on host {host}'.format(
478             host=node['host'])
479         with PapiExecutor(node) as papi_exec:
480             papi_resp = papi_exec.add(cmd, **args).get_dump(err_msg)
481
482         data = papi_resp.reply[0]['api_reply']
483
484         fib_data = list()
485         for fib in data:
486             fib_item = fib[cmd_reply]
487             fib_item['mac'] = L2Util.bin_to_mac(fib_item['mac'])
488             fib_data.append(fib_item)
489
490         return fib_data
491
492     @staticmethod
493     def get_l2_fib_entry_by_mac(node, bd_index, mac):
494         """Retrieves the L2 FIB entry specified by MAC address using PAPI.
495
496         :param node: VPP node.
497         :param bd_index: Index of the bridge domain.
498         :param mac: MAC address used as the key in L2 FIB data structure.
499         :type node: dict
500         :type bd_index: int
501         :type mac: str
502         :returns: L2 FIB entry
503         :rtype: dict
504         """
505
506         bd_data = L2Util.vpp_get_bridge_domain_data(node)
507         bd_id = bd_data[bd_index-1]['bd_id']
508
509         table = L2Util.get_l2_fib_table(node, bd_id)
510
511         for entry in table:
512             if entry['mac'] == mac:
513                 return entry
514         return {}