aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/Map.py
blob: 7e4d219bdd8dabc61d737611aac2ebc24ff221d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# Copyright (c) 2016 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Map utilities library."""


import ipaddress

from resources.libraries.python.VatExecutor import VatExecutor


class Map(object):
    """Utilities for manipulating MAP feature in VPP."""

    @staticmethod
    def map_add_domain(vpp_node, ip4_pfx, ip6_pfx, ip6_src, ea_bits_len,
                       psid_offset, psid_len, map_t=False):
        """Add map domain on node.

        :param vpp_node: VPP node to add map domain on.
        :param ip4_pfx: Rule IPv4 prefix.
        :param ip6_pfx: Rule IPv6 prefix.
        :param ip6_src: MAP domain IPv6 BR address / Tunnel source.
        :param ea_bits_len: Embedded Address bits length.
        :param psid_offset: Port Set Identifier (PSID) offset.
        :param psid_len: Port Set Identifier (PSID) length.
        :param map_t: Mapping using translation instead of encapsulation.
        Default False.
        :type vpp_node: dict
        :type ip4_pfx: str
        :type ip6_pfx: str
        :type ip6_src: str
        :type ea_bits_len: int
        :type psid_offset: int
        :type psid_len: int
        :type map_t: bool
        :returns: Index of created map domain.
        :rtype: int
        :raises RuntimeError: If unable to add map domain.
        """
        translate = 'map-t' if map_t else ''

        output = VatExecutor.cmd_from_template(vpp_node, "map_add_domain.vat",
                                               ip4_pfx=ip4_pfx,
                                               ip6_pfx=ip6_pfx,
                                               ip6_src=ip6_src,
                                               ea_bits_len=ea_bits_len,
                                               psid_offset=psid_offset,
                                               psid_len=psid_len,
                                               map_t=translate)
        if output[0]["retval"] == 0:
            return output[0]["index"]
        else:
            raise RuntimeError('Unable to add map domain on node {}'
                               .format(vpp_node['host']))

    @staticmethod
    def map_add_rule(vpp_node, index, psid, dst, delete=False):
        """Add or delete map rule on node.

        :param vpp_node: VPP node to add map rule on.
        :param index: Map domain index to add rule to.
        :param psid: Port Set Identifier.
        :param dst: MAP CE IPv6 address.
        :param delete: If set to True, delete rule. Default False.
        :type vpp_node: dict
        :type index: int
        :type psid: int
        :type dst: str
        :type delete: bool
        :raises RuntimeError: If unable to add map rule.
        """
        output = VatExecutor.cmd_from_template(vpp_node, "map_add_del_rule.vat",
                                               index=index,
                                               psid=psid,
                                               dst=dst,
                                               delete='del' if delete else '')

        if output[0]["retval"] != 0:
            raise RuntimeError('Unable to add map rule on node {}'
                               .format(vpp_node['host']))

    @staticmethod
    def map_del_domain(vpp_node, index):
        """Delete map domain on node.

        :param vpp_node: VPP node to delete map domain on.
        :param index: Index of the map domain.
        :type vpp_node: dict
        :type index: int
        :raises RuntimeError: If unable to delete map domain.
        """
        output = VatExecutor.cmd_from_template(vpp_node, "map_del_domain.vat",
                                               index=index)
        if output[0]["retval"] != 0:
            raise RuntimeError('Unable to delete map domain {} on node {}'
                               .format(index, vpp_node['host']))

    @staticmethod
    def get_psid_from_port(port, psid_len, psid_offset):
        """Return PSID from port.
                              0                   1
                              0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
                             +-----------+-----------+-------+
               Ports in      |     A     |    PSID   |   j   |
            the CE port set  |    > 0    |           |       |
                             +-----------+-----------+-------+
                             |  a bits   |  k bits   |m bits |


        :param port: Port to compute PSID from.
        :param psid_len: PSID length.
        :param psid_offset: PSID offset.
        :type port: int
        :type psid_len: int
        :type psid_offset: int
        :returns: PSID.
        :rtype: int
        """
        ones = 2**16-1
        mask = ones >> (16 - psid_len)
        psid = port >> (16 - psid_len - psid_offset)
        psid &= mask
        return psid

    @staticmethod
    def _make_ea_bits(ipv4_net, ipv4_host, ea_bit_len, psid_len, psid):
        """
        _note_: host(or prefix) part of destination ip in rule prefix, + psid

        :param ipv4_net: IPv4 domain prefix.
        :param ipv4_host: Destination IPv4 address.
        :param ea_bit_len: EA bit length.
        :param psid_len: PSID length.
        :param psid: PSID.
        :type ipv4_net: ipaddress.IPv4Network
        :type ipv4_host: ipaddress.IPv4Address
        :type ea_bit_len: int
        :type psid_len: int
        :type psid: int
        :returns: Number representing EA bit field of destination IPv6 address.
        :rtype: int
        """
        v4_suffix_len = ipv4_net.max_prefixlen - ipv4_net.prefixlen
        v4_suffix = int(ipv4_net.network_address) ^ int(ipv4_host)

        if ipv4_net.prefixlen + ea_bit_len <= 32:
            ea_bits = v4_suffix >> (v4_suffix_len - ea_bit_len)
            return ea_bits
        else:
            q_len = ea_bit_len - v4_suffix_len
            # p_bits = v4_suffix << q_len  # option 1: psid right padded
            p_bits = v4_suffix << psid_len  # option 2: psid left padded
            if q_len < psid_len:
                raise Exception("invalid configuration: q_len < psid_len")
            ea_bits = p_bits | psid
            ea_bits <<= q_len - psid_len  # option 2: psid left padded
            return ea_bits

    @staticmethod
    def _make_interface_id(rule_net, dst_ip, ea_bit_len, psid):
        """
        _note_: if prefix or complete ip (<= 32), psid is 0

        :param rule_net: IPv4 domain prefix.
        :param dst_ip: Destination IPv4 address.
        :param ea_bit_len: EA bit length.
        :param psid: PSID.
        :type rule_net: ipaddress.IPv4Network
        :type dst_ip: ipaddress.IPv4Address
        :type ea_bit_len: int
        :type psid: int
        :returns: Number representing interface id field of destination IPv6
        address.
        :rtype: int
        """
        if rule_net.prefixlen + ea_bit_len < 32:
            v4_suffix_len = rule_net.max_prefixlen - rule_net.prefixlen
            v4_suffix = int(rule_net.network_address) ^ int(dst_ip)
            ea_bits = v4_suffix >> (v4_suffix_len - ea_bit_len)
            address = int(rule_net.network_address) >> v4_suffix_len
            address <<= ea_bit_len
            address |= ea_bits
            address <<= 32 - rule_net.prefixlen - ea_bit_len
            address <<= 16
        elif rule_net.prefixlen + ea_bit_len == 32:
            address = int(dst_ip) << 16
        else:
            address = int(dst_ip) << 16
            address |= psid
            return address

        return address

    @staticmethod
    def compute_ipv6_map_destination_address(ipv4_pfx, ipv6_pfx, ea_bit_len,
                                             psid_offset, psid_len, ipv4_dst,
                                             dst_port):
        """Compute IPv6 destination address from IPv4 address for MAP algorithm.
        (RFC 7597)

       |     n bits         |  o bits   | s bits  |   128-n-o-s bits      |
       +--------------------+-----------+---------+-----------------------+
       |  Rule IPv6 prefix  |  EA bits  |subnet ID|     interface ID      |
       +--------------------+-----------+---------+-----------------------+
       |<---  End-user IPv6 prefix  --->|


        :param ipv4_pfx: Domain IPv4 preffix.
        :param ipv6_pfx: Domain IPv6 preffix.
        :param ea_bit_len: Domain EA bits length.
        :param psid_offset: Domain PSID offset.
        :param psid_len: Domain PSID length.
        :param ipv4_dst: Destination IPv4 address.
        :param dst_port: Destination port number or ICMP ID.
        :type ipv4_pfx: str
        :type ipv6_pfx: str
        :type ea_bit_len: int
        :type psid_offset: int
        :type psid_len: int
        :type ipv4_dst: str
        :type dst_port: int
        :returns: Computed IPv6 address.
        :rtype: str
        """
        ipv6_net = ipaddress.ip_network(unicode(ipv6_pfx))
        ipv4_net = ipaddress.ip_network(unicode(ipv4_pfx))
        ipv4_host = ipaddress.ip_address(unicode(ipv4_dst))

        ipv6_host_len = ipv6_net.max_prefixlen - ipv6_net.prefixlen
        end_user_v6_pfx_len = ipv6_net.prefixlen + ea_bit_len
        psid = Map.get_psid_from_port(dst_port, psid_len, psid_offset)

        rule_v6_pfx = int(ipv6_net.network_address) >> ipv6_host_len
        ea_bits = Map._make_ea_bits(ipv4_net, ipv4_host, ea_bit_len, psid_len,
                                    psid)
        interface_id = Map._make_interface_id(ipv4_net, ipv4_host, ea_bit_len,
                                              psid)

        address = rule_v6_pfx << ea_bit_len
        address |= ea_bits  # add EA bits

        if end_user_v6_pfx_len > 64:
            # If the End-user IPv6 prefix length is larger than 64,
            # the most significant parts of the interface identifier are
            # overwritten by the prefix.
            mask = (2**128-1) >> end_user_v6_pfx_len
            interface_id &= mask
        address <<= (128 - end_user_v6_pfx_len)
        address |= interface_id  # add Interface ID bits

        return str(ipaddress.ip_address(address))

    @staticmethod
    def compute_ipv6_map_source_address(ipv6_pfx, ipv4_src):
        """Compute IPv6 source address from IPv4 address for MAP-T algorithm.

        :param ipv6_pfx: 96 bit long IPv6 prefix.
        :param ipv4_src: IPv4 source address
        :type ipv6_pfx: str
        :type ipv4_src: str
        :returns: IPv6 address, combination of IPv6 prefix and IPv4 address.
        :rtype: str
        """
        ipv6_net = ipaddress.ip_network(unicode(ipv6_pfx))
        ipv4_host = ipaddress.ip_address(unicode(ipv4_src))

        address = int(ipv6_net.network_address)
        address |= int(ipv4_host)

        return str(ipaddress.ip_address(address))
rmat(data[1]['/if/rx'][0][0])) Note: In this case, when PapiExecutor method 'add' is used: - its parameter 'csit_papi_command' is used only to keep information that vpp-stats are requested. It is not further processed but it is included in the PAPI history this way: vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input']) Always use csit_papi_command="vpp-stats" if the VPP PAPI method is "stats". - the second parameter must be 'path' as it is used by PapiExecutor method 'add'. """ def __init__(self, node): """Initialization. :param node: Node to run command(s) on. :type node: dict """ # Node to run command(s) on. self._node = node # The list of PAPI commands to be executed on the node. self._api_command_list = list() self._ssh = SSH() def __enter__(self): try: self._ssh.connect(self._node) except IOError: raise RuntimeError("Cannot open SSH connection to host {host} to " "execute PAPI command(s)". format(host=self._node["host"])) return self def __exit__(self, exc_type, exc_val, exc_tb): self._ssh.disconnect(self._node) def add(self, csit_papi_command="vpp-stats", **kwargs): """Add next command to internal command list; return self. The argument name 'csit_papi_command' must be unique enough as it cannot be repeated in kwargs. :param csit_papi_command: VPP API command. :param kwargs: Optional key-value arguments. :type csit_papi_command: str :type kwargs: dict :returns: self, so that method chaining is possible. :rtype: PapiExecutor """ PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs) self._api_command_list.append(dict(api_name=csit_papi_command, api_args=kwargs)) return self def get_stats(self, err_msg="Failed to get statistics.", timeout=120): """Get VPP Stats from VPP Python API. :param err_msg: The message used if the PAPI command(s) execution fails. :param timeout: Timeout in seconds. :type err_msg: str :type timeout: int :returns: Requested VPP statistics. :rtype: list """ paths = [cmd['api_args']['path'] for cmd in self._api_command_list] self._api_command_list = list() ret_code, stdout, _ = self._execute_papi(paths, method='stats', err_msg=err_msg, timeout=timeout) return json.loads(stdout) def get_replies(self, err_msg="Failed to get replies.", process_reply=True, ignore_errors=False, timeout=120): """Get reply/replies from VPP Python API. :param err_msg: The message used if the PAPI command(s) execution fails. :param process_reply: Process PAPI reply if True. :param ignore_errors: If true, the errors in the reply are ignored. :param timeout: Timeout in seconds. :type err_msg: str :type process_reply: bool :type ignore_errors: bool :type timeout: int :returns: Papi response including: papi reply, stdout, stderr and return code. :rtype: PapiResponse """ response = self._execute(method='request', process_reply=process_reply, ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout) return response def get_dump(self, err_msg="Failed to get dump.", process_reply=True, ignore_errors=False, timeout=120): """Get dump from VPP Python API. :param err_msg: The message used if the PAPI command(s) execution fails. :param process_reply: Process PAPI reply if True. :param ignore_errors: If true, the errors in the reply are ignored. :param timeout: Timeout in seconds. :type err_msg: str :type process_reply: bool :type ignore_errors: bool :type timeout: int :returns: Papi response including: papi reply, stdout, stderr and return code. :rtype: PapiResponse """ response = self._execute(method='dump', process_reply=process_reply, ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout) return response def execute_should_pass(self, err_msg="Failed to execute PAPI command.", process_reply=True, ignore_errors=False, timeout=120): """Execute the PAPI commands and check the return code. Raise exception if the PAPI command(s) failed. IMPORTANT! Do not use this method in L1 keywords. Use: - get_replies() - get_dump() This method will be removed soon. :param err_msg: The message used if the PAPI command(s) execution fails. :param process_reply: Indicate whether or not to process PAPI reply. :param ignore_errors: If true, the errors in the reply are ignored. :param timeout: Timeout in seconds. :type err_msg: str :type process_reply: bool :type ignore_errors: bool :type timeout: int :returns: Papi response including: papi reply, stdout, stderr and return code. :rtype: PapiResponse :raises AssertionError: If PAPI command(s) execution failed. """ response = self.get_replies(process_reply=process_reply, ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout) return response @staticmethod def _process_api_data(api_d): """Process API data for smooth converting to JSON string. Apply binascii.hexlify() method for string values. :param api_d: List of APIs with their arguments. :type api_d: list :returns: List of APIs with arguments pre-processed for JSON. :rtype: list """ api_data_processed = list() for api in api_d: api_args_processed = dict() for a_k, a_v in api["api_args"].iteritems(): value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v api_args_processed[str(a_k)] = value api_data_processed.append(dict(api_name=api["api_name"], api_args=api_args_processed)) return api_data_processed @staticmethod def _revert_api_reply(api_r): """Process API reply / a part of API reply. Apply binascii.unhexlify() method for unicode values. TODO: Implement complex solution to process of replies. :param api_r: API reply. :type api_r: dict :returns: Processed API reply / a part of API reply. :rtype: dict """ reply_dict = dict() reply_value = dict() for reply_key, reply_v in api_r.iteritems(): for a_k, a_v in reply_v.iteritems(): reply_value[a_k] = binascii.unhexlify(a_v) \ if isinstance(a_v, unicode) else a_v reply_dict[reply_key] = reply_value return reply_dict def _process_reply(self, api_reply): """Process API reply. :param api_reply: API reply. :type api_reply: dict or list of dict :returns: Processed API reply. :rtype: list or dict """ if isinstance(api_reply, list): reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply] else: reverted_reply = self._revert_api_reply(api_reply) return reverted_reply def _execute_papi(self, api_data, method='request', err_msg="", timeout=120): """Execute PAPI command(s) on remote node and store the result. :param api_data: List of APIs with their arguments. :param method: VPP Python API method. Supported methods are: 'request', 'dump' and 'stats'. :param err_msg: The message used if the PAPI command(s) execution fails. :param timeout: Timeout in seconds. :type api_data: list :type method: str :type err_msg: str :type timeout: int :raises SSHTimeout: If PAPI command(s) execution has timed out. :raises RuntimeError: If PAPI executor failed due to another reason. :raises AssertionError: If PAPI command(s) execution has failed. """ if not api_data: RuntimeError("No API data provided.") json_data = json.dumps(api_data) if method == "stats" \ else json.dumps(self._process_api_data(api_data)) cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\ format(fw_dir=Constants.REMOTE_FW_DIR, papi_provider=Constants.RESOURCES_PAPI_PROVIDER, method=method, json=json_data) try: ret_code, stdout, stderr = self._ssh.exec_command_sudo( cmd=cmd, timeout=timeout) except SSHTimeout: logger.error("PAPI command(s) execution timeout on host {host}:" "\n{apis}".format(host=self._node["host"], apis=api_data)) raise except Exception: raise RuntimeError("PAPI command(s) execution on host {host} " "failed: {apis}".format(host=self._node["host"], apis=api_data)) if ret_code != 0: raise AssertionError(err_msg) return ret_code, stdout, stderr def _execute(self, method='request', process_reply=True, ignore_errors=False, err_msg="", timeout=120): """Turn internal command list into proper data and execute; return PAPI response. This method also clears the internal command list. IMPORTANT! Do not use this method in L1 keywords. Use: - get_stats() - get_replies() - get_dump() :param method: VPP Python API method. Supported methods are: 'request', 'dump' and 'stats'. :param process_reply: Process PAPI reply if True. :param ignore_errors: If true, the errors in the reply are ignored. :param err_msg: The message used if the PAPI command(s) execution fails. :param timeout: Timeout in seconds. :type method: str :type process_reply: bool :type ignore_errors: bool :type err_msg: str :type timeout: int :returns: Papi response including: papi reply, stdout, stderr and return code. :rtype: PapiResponse :raises KeyError: If the reply is not correct. """ local_list = self._api_command_list # Clear first as execution may fail. self._api_command_list = list() ret_code, stdout, stderr = self._execute_papi(local_list, method=method, err_msg=err_msg, timeout=timeout) papi_reply = list() if process_reply: try: json_data = json.loads(stdout) except ValueError: logger.error("An error occured while processing the PAPI " "request:\n{rqst}".format(rqst=local_list)) raise for data in json_data: try: api_reply_processed = dict( api_name=data["api_name"], api_reply=self._process_reply(data["api_reply"])) except KeyError: if ignore_errors: continue else: raise papi_reply.append(api_reply_processed) # Log processed papi reply to be able to check API replies changes logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply)) return PapiResponse(papi_reply=papi_reply, stdout=stdout, stderr=stderr, ret_code=ret_code, requests=[rqst["api_name"] for rqst in local_list])