Passed
Pull Request — master (#108)
by
unknown
02:50
created

build.main.Main.do_match()   C

Complexity

Conditions 11

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 11.0795

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 28
ccs 21
cts 23
cp 0.913
rs 5.4
c 0
b 0
f 0
cc 11
nop 4
crap 11.0795

How to fix   Complexity   

Complexity

Complex classes like build.main.Main.do_match() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Main module of amlight/sdntrace_cp Kytos Network Application.
2
3
Run tracepaths on OpenFlow in the Control Plane
4
"""
5
6 1
import pathlib
7 1
from datetime import datetime
8
9 1
import tenacity
10 1
from kytos.core import KytosNApp, log, rest
11 1
from kytos.core.helpers import load_spec, validate_openapi
12 1
from kytos.core.rest_api import (HTTPException, JSONResponse, Request,
13
                                 get_json_or_400)
14 1
from napps.amlight.sdntrace_cp.utils import (convert_entries,
15
                                             convert_list_entries,
16
                                             find_endpoint, get_stored_flows,
17
                                             match_field_dl_vlan,
18
                                             match_field_ip, prepare_json)
19
20
21 1
class Main(KytosNApp):
22
    """Main class of amlight/sdntrace_cp NApp.
23
24
    This application gets the list of flows from the switches
25
    and uses it to trace paths without using the data plane.
26
    """
27
28 1
    spec = load_spec(pathlib.Path(__file__).parent / "openapi.yml")
29
30 1
    def setup(self):
31
        """Replace the '__init__' method for the KytosNApp subclass.
32
33
        The setup method is automatically called by the controller when your
34
        application is loaded.
35
36
        """
37 1
        log.info("Starting Kytos SDNTrace CP App!")
38
39 1
    def execute(self):
40
        """This method is executed right after the setup method execution.
41
42
        You can also use this method in loop mode if you add to the above setup
43
        method a line like the following example:
44
45
            self.execute_as_loop(30)  # 30-second interval.
46
        """
47
48 1
    def shutdown(self):
49
        """This method is executed when your napp is unloaded.
50
51
        If you have some cleanup procedure, insert it here.
52
        """
53
54 1 View Code Duplication
    @rest('/v1/trace', methods=['PUT'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
55 1
    @validate_openapi(spec)
56 1
    def trace(self, request: Request) -> JSONResponse:
57
        """Trace a path."""
58 1
        result = []
59 1
        data = get_json_or_400(request, self.controller.loop)
60 1
        entries = convert_entries(data)
61 1
        if not entries:
62
            raise HTTPException(400, "Empty entries")
63 1
        try:
64 1
            stored_flows = get_stored_flows()
65
        except tenacity.RetryError as exc:
66
            raise HTTPException(424, "It couldn't get stored_flows") from exc
67 1
        try:
68 1
            result = self.tracepath(entries, stored_flows)
69
        except ValueError as exception:
70
            log.debug("tracepath error {exception}")
71
            raise exception
72 1
        return JSONResponse(prepare_json(result))
73
74 1 View Code Duplication
    @rest('/v1/traces', methods=['PUT'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
75 1
    @validate_openapi(spec)
76 1
    def get_traces(self, request: Request) -> JSONResponse:
77
        """For bulk requests."""
78 1
        data = get_json_or_400(request, self.controller.loop)
79 1
        entries = convert_list_entries(data)
80 1
        results = []
81 1
        try:
82 1
            stored_flows = get_stored_flows()
83 1
        except tenacity.RetryError as exc:
84
            raise HTTPException(424, "It couldn't get stored_flows") from exc
85 1
        for entry in entries:
86 1
            try:
87 1
                results.append(self.tracepath(entry, stored_flows))
88 1
            except ValueError as exception:
89 1
                log.debug("tracepath error {exception}")
90 1
                raise exception
91 1
        return JSONResponse(prepare_json(results))
92
93 1
    def tracepath(self, entries, stored_flows):
94
        """Trace a path for a packet represented by entries."""
95
        # pylint: disable=too-many-branches
96 1
        trace_result = []
97 1
        trace_type = 'starting'
98 1
        do_trace = True
99 1
        while do_trace:
100 1
            if 'dpid' not in entries or 'in_port' not in entries:
101
                break
102 1
            trace_step = {'in': {'dpid': entries['dpid'],
103
                                 'port': entries['in_port'],
104
                                 'time': str(datetime.now()),
105
                                 'type': trace_type}}
106 1
            if 'dl_vlan' in entries:
107 1
                trace_step['in'].update({'vlan': entries['dl_vlan'][-1]})
108
109 1
            switch = self.controller.get_switch_by_dpid(entries['dpid'])
110 1
            if not switch:
111 1
                trace_step['in']['type'] = 'last'
112 1
                trace_result.append(trace_step)
113 1
                break
114 1
            result = self.trace_step(switch, entries, stored_flows)
115 1
            if result:
116 1
                out = {'port': result['out_port']}
117 1
                if 'dl_vlan' in result['entries']:
118 1
                    out.update({'vlan': result['entries']['dl_vlan'][-1]})
119 1
                trace_step.update({
120
                    'out': out
121
                })
122 1
                if 'dpid' in result:
123 1
                    next_step = {'dpid': result['dpid'],
124
                                 'port': result['in_port']}
125 1
                    entries = result['entries']
126 1
                    entries['dpid'] = result['dpid']
127 1
                    entries['in_port'] = result['in_port']
128 1
                    if self.has_loop(next_step, trace_result):
129 1
                        trace_step['in']['type'] = 'loop'
130 1
                        do_trace = False
131
                    else:
132 1
                        trace_type = 'intermediary'
133
                else:
134 1
                    trace_step['in']['type'] = 'last'
135 1
                    do_trace = False
136
            else:
137
                # No match
138
                break
139 1
            if 'out' in trace_step and trace_step['out']:
140 1
                if self.check_loop_trace_step(trace_step, trace_result):
141 1
                    do_trace = False
142 1
            trace_result.append(trace_step)
143 1
        if len(trace_result) == 1 and \
144
                trace_result[0]['in']['type'] == 'starting':
145
            trace_result[0]['in']['type'] = 'last'
146 1
        return trace_result
147
148 1
    @staticmethod
149 1
    def check_loop_trace_step(trace_step, trace_result):
150
        """Check if there is a loop in the trace and add the step."""
151
        # outgoing interface is the same as the input interface
152 1
        if not trace_result and \
153
                trace_step['in']['type'] == 'last' and \
154
                trace_step['in']['port'] == trace_step['out']['port']:
155 1
            trace_step['in']['type'] = 'loop'
156 1
            return True
157 1
        if trace_result and \
158
                trace_result[0]['in']['dpid'] == trace_step['in']['dpid'] and \
159
                trace_result[0]['in']['port'] == trace_step['out']['port']:
160 1
            trace_step['in']['type'] = 'loop'
161 1
            return True
162 1
        return False
163
164 1
    @staticmethod
165 1
    def has_loop(trace_step, trace_result):
166
        """Check if there is a loop in the trace result."""
167 1
        for trace in trace_result:
168 1
            if trace['in']['dpid'] == trace_step['dpid'] and \
169
                            trace['in']['port'] == trace_step['port']:
170 1
                return True
171 1
        return False
172
173 1
    def trace_step(self, switch, entries, stored_flows):
174
        """Perform a trace step.
175
176
        Match the given fields against the switch's list of flows."""
177 1
        flow, entries, port = self.match_and_apply(
178
                                                    switch,
179
                                                    entries,
180
                                                    stored_flows
181
                                                )
182
183 1
        if not flow or not port:
184 1
            return None
185
186 1
        endpoint = find_endpoint(switch, port)
187 1
        if endpoint is None:
188
            log.warning(f"Port {port} not found on switch {switch}")
189
            return None
190 1
        endpoint = endpoint['endpoint']
191 1
        if endpoint is None:
192 1
            return {'out_port': port,
193
                    'entries': entries}
194
195 1
        return {'dpid': endpoint.switch.dpid,
196
                'in_port': endpoint.port_number,
197
                'out_port': port,
198
                'entries': entries}
199
200 1
    @classmethod
201 1
    def do_match(cls, flow, args, table_id):
202
        """Match a packet against this flow (OF1.3)."""
203
        # pylint: disable=consider-using-dict-items
204
        # pylint: disable=too-many-return-statements
205 1
        if ('match' not in flow['flow']) or (len(flow['flow']['match']) == 0):
206 1
            return False
207 1
        table_id_ = flow['flow'].get('table_id', 0)
208 1
        if table_id != table_id_:
209 1
            return False
210 1
        for name in flow['flow']['match']:
211 1
            field_flow = flow['flow']['match'][name]
212 1
            field = args.get(name)
213 1
            if name == 'dl_vlan':
214 1
                if not match_field_dl_vlan(field, field_flow):
215 1
                    return False
216
                continue
217
            # In the case of dl_vlan field, the match must be checked
218
            # even if this field is not in the packet args.
219 1
            if not field:
220 1
                return False
221 1
            if name in ('nw_src', 'nw_dst', 'ipv6_src', 'ipv6_dst'):
222 1
                if not match_field_ip(field, field_flow):
223 1
                    return False
224
                continue
225 1
            if field_flow != field:
226 1
                return False
227 1
        return flow
228
229
    # pylint: disable=too-many-arguments
230 1
    def match_flows(self, switch, table_id, args, stored_flows, many=True):
231
        """
232
        Match the packet in request against the stored flows from flow_manager.
233
        Try the match with each flow, in other. If many is True, tries the
234
        match with all flows, if False, tries until the first match.
235
        :param args: packet data
236
        :param many: Boolean, indicating whether to continue after matching the
237
                first flow or not
238
        :return: If many, the list of matched flows, or the matched flow
239
        """
240 1
        if switch.dpid not in stored_flows:
241 1
            return None
242 1
        response = []
243 1
        if switch.dpid not in stored_flows:
244
            return None
245 1
        try:
246 1
            for flow in stored_flows[switch.dpid]:
247 1
                match = Main.do_match(flow, args, table_id)
248 1
                if match:
249 1
                    if many:
250 1
                        response.append(match)
251
                    else:
252 1
                        response = match
253 1
                        break
254
        except AttributeError:
255
            return None
256 1
        if not many and isinstance(response, list):
257 1
            return None
258 1
        return response
259
260 1
    def process_tables(self, switch, table_id, args, stored_flows, actions):
261
        """Resolve the table context and get actions in the matched flow"""
262 1
        goto_table = False
263 1
        actions_ = []
264 1
        flow = self.match_flows(switch, table_id, args, stored_flows, False)
265 1
        if flow and 'actions' in flow['flow']:
266 1
            actions_ = flow['flow']['actions']
267 1
        elif flow and 'instructions' in flow['flow']:
268 1
            for instruction in flow['flow']['instructions']:
269 1
                if instruction['instruction_type'] == 'apply_actions':
270 1
                    actions_ = instruction['actions']
271 1
                elif instruction['instruction_type'] == 'goto_table':
272 1
                    table_id_ = instruction['table_id']
273 1
                    if table_id < table_id_:
274 1
                        table_id = table_id_
275 1
                        goto_table = True
276
                    else:
277 1
                        msg = f"Wrong table_id in {flow['flow']}: \
278
                            The packet can only been directed to a \
279
                                flow table number greather than {table_id}"
280 1
                        raise ValueError(msg) from ValueError
281 1
        actions.extend(actions_)
282 1
        return flow, actions, goto_table, table_id
283
284 1
    def match_and_apply(self, switch, args, stored_flows):
285
        """Match flows and apply actions.
286
        Match given packet (in args) against
287
        the stored flows (from flow_manager) and,
288
        if a match flow is found, apply its actions."""
289 1
        table_id = 0
290 1
        goto_table = True
291 1
        port = None
292 1
        actions = []
293 1
        while goto_table:
294 1
            try:
295 1
                flow, actions, goto_table, table_id = self.process_tables(
296
                    switch, table_id, args, stored_flows, actions)
297 1
            except ValueError as exception:
298 1
                raise exception
299 1
        if not flow or switch.ofp_version != '0x04':
0 ignored issues
show
introduced by
The variable flow does not seem to be defined in case the while loop on line 293 is not entered. Are you sure this can never be the case?
Loading history...
300 1
            return flow, args, port
301
302 1
        for action in actions:
303 1
            action_type = action['action_type']
304 1
            if action_type == 'output':
305 1
                port = action['port']
306 1
            if action_type == 'push_vlan':
307 1
                if 'dl_vlan' not in args:
308
                    args['dl_vlan'] = []
309 1
                args['dl_vlan'].append(0)
310 1
            if action_type == 'pop_vlan':
311 1
                if 'dl_vlan' in args:
312 1
                    args['dl_vlan'].pop()
313 1
                    if len(args['dl_vlan']) == 0:
314 1
                        del args['dl_vlan']
315 1
            if action_type == 'set_vlan':
316 1
                args['dl_vlan'][-1] = action['vlan_id']
317
        return flow, args, port
318