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

build.main.Main.get_traces()   A

Complexity

Conditions 3

Size

Total Lines 14
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.004

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 14
rs 9.75
c 0
b 0
f 0
ccs 12
cts 13
cp 0.9231
cc 3
nop 2
crap 3.004
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
    @rest('/v1/trace', methods=['PUT'])
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
        result = self.tracepath(entries, stored_flows)
68 1
        return JSONResponse(prepare_json(result))
69
70 1
    @rest('/v1/traces', methods=['PUT'])
71 1
    @validate_openapi(spec)
72 1
    def get_traces(self, request: Request) -> JSONResponse:
73
        """For bulk requests."""
74 1
        data = get_json_or_400(request, self.controller.loop)
75 1
        entries = convert_list_entries(data)
76 1
        results = []
77 1
        try:
78 1
            stored_flows = get_stored_flows()
79 1
        except tenacity.RetryError as exc:
80
            raise HTTPException(424, "It couldn't get stored_flows") from exc
81 1
        for entry in entries:
82 1
            results.append(self.tracepath(entry, stored_flows))
83 1
        return JSONResponse(prepare_json(results))
84
85 1
    def tracepath(self, entries, stored_flows):
86
        """Trace a path for a packet represented by entries."""
87
        # pylint: disable=too-many-branches
88 1
        trace_result = []
89 1
        trace_type = 'starting'
90 1
        do_trace = True
91 1
        while do_trace:
92 1
            if 'dpid' not in entries or 'in_port' not in entries:
93
                break
94 1
            trace_step = {'in': {'dpid': entries['dpid'],
95
                                 'port': entries['in_port'],
96
                                 'time': str(datetime.now()),
97
                                 'type': trace_type}}
98 1
            if 'dl_vlan' in entries:
99 1
                trace_step['in'].update({'vlan': entries['dl_vlan'][-1]})
100
101 1
            switch = self.controller.get_switch_by_dpid(entries['dpid'])
102 1
            if not switch:
103 1
                trace_step['in']['type'] = 'last'
104 1
                trace_result.append(trace_step)
105 1
                break
106 1
            result = self.trace_step(switch, entries, stored_flows)
107 1
            if result:
108 1
                out = {'port': result['out_port']}
109 1
                if 'dl_vlan' in result['entries']:
110 1
                    out.update({'vlan': result['entries']['dl_vlan'][-1]})
111 1
                trace_step.update({
112
                    'out': out
113
                })
114 1
                if 'dpid' in result:
115 1
                    next_step = {'dpid': result['dpid'],
116
                                 'port': result['in_port']}
117 1
                    entries = result['entries']
118 1
                    entries['dpid'] = result['dpid']
119 1
                    entries['in_port'] = result['in_port']
120 1
                    if self.has_loop(next_step, trace_result):
121 1
                        trace_step['in']['type'] = 'loop'
122 1
                        do_trace = False
123
                    else:
124 1
                        trace_type = 'intermediary'
125
                else:
126 1
                    trace_step['in']['type'] = 'last'
127 1
                    do_trace = False
128
            else:
129
                # No match
130
                break
131 1
            if 'out' in trace_step and trace_step['out']:
132 1
                if self.check_loop_trace_step(trace_step, trace_result):
133 1
                    do_trace = False
134 1
            trace_result.append(trace_step)
135 1
        if len(trace_result) == 1 and \
136
                trace_result[0]['in']['type'] == 'starting':
137
            trace_result[0]['in']['type'] = 'last'
138 1
        return trace_result
139
140 1
    @staticmethod
141 1
    def check_loop_trace_step(trace_step, trace_result):
142
        """Check if there is a loop in the trace and add the step."""
143
        # outgoing interface is the same as the input interface
144 1
        if not trace_result and \
145
                trace_step['in']['type'] == 'last' and \
146
                trace_step['in']['port'] == trace_step['out']['port']:
147 1
            trace_step['in']['type'] = 'loop'
148 1
            return True
149 1
        if trace_result and \
150
                trace_result[0]['in']['dpid'] == trace_step['in']['dpid'] and \
151
                trace_result[0]['in']['port'] == trace_step['out']['port']:
152 1
            trace_step['in']['type'] = 'loop'
153 1
            return True
154 1
        return False
155
156 1
    @staticmethod
157 1
    def has_loop(trace_step, trace_result):
158
        """Check if there is a loop in the trace result."""
159 1
        for trace in trace_result:
160 1
            if trace['in']['dpid'] == trace_step['dpid'] and \
161
                            trace['in']['port'] == trace_step['port']:
162 1
                return True
163 1
        return False
164
165 1
    def trace_step(self, switch, entries, stored_flows):
166
        """Perform a trace step.
167
168
        Match the given fields against the switch's list of flows."""
169 1
        flow, entries, port = self.match_and_apply(
170
                                                    switch,
171
                                                    entries,
172
                                                    stored_flows
173
                                                )
174
175 1
        if not flow or not port:
176 1
            return None
177
178 1
        endpoint = find_endpoint(switch, port)
179 1
        if endpoint is None:
180
            log.warning(f"Port {port} not found on switch {switch}")
181
            return None
182 1
        endpoint = endpoint['endpoint']
183 1
        if endpoint is None:
184 1
            return {'out_port': port,
185
                    'entries': entries}
186
187 1
        return {'dpid': endpoint.switch.dpid,
188
                'in_port': endpoint.port_number,
189
                'out_port': port,
190
                'entries': entries}
191
192 1
    @classmethod
193 1
    def do_match(cls, flow, args, table_id):
194
        """Match a packet against this flow (OF1.3)."""
195
        # pylint: disable=consider-using-dict-items
196
        # pylint: disable=too-many-return-statements
197 1
        if ('match' not in flow['flow']) or (len(flow['flow']['match']) == 0):
198 1
            return False
199 1
        table_id_ = flow['flow'].get('table_id', 0)
200 1
        if table_id != table_id_:
201 1
            return False
202 1
        for name in flow['flow']['match']:
203 1
            field_flow = flow['flow']['match'][name]
204 1
            field = args.get(name)
205 1
            if name == 'dl_vlan':
206 1
                if not match_field_dl_vlan(field, field_flow):
207 1
                    return False
208
                continue
209
            # In the case of dl_vlan field, the match must be checked
210
            # even if this field is not in the packet args.
211 1
            if not field:
212 1
                return False
213 1
            if name in ('nw_src', 'nw_dst', 'ipv6_src', 'ipv6_dst'):
214 1
                if not match_field_ip(field, field_flow):
215 1
                    return False
216
                continue
217 1
            if field_flow != field:
218 1
                return False
219 1
        return flow
220
221
    # pylint: disable=too-many-arguments
222 1
    def match_flows(self, switch, table_id, args, stored_flows, many=True):
223
        """
224
        Match the packet in request against the stored flows from flow_manager.
225
        Try the match with each flow, in other. If many is True, tries the
226
        match with all flows, if False, tries until the first match.
227
        :param args: packet data
228
        :param many: Boolean, indicating whether to continue after matching the
229
                first flow or not
230
        :return: If many, the list of matched flows, or the matched flow
231
        """
232 1
        if switch.dpid not in stored_flows:
233 1
            return None
234 1
        response = []
235 1
        if switch.dpid not in stored_flows:
236
            return None
237 1
        try:
238 1
            for flow in stored_flows[switch.dpid]:
239 1
                match = Main.do_match(flow, args, table_id)
240 1
                if match:
241 1
                    if many:
242 1
                        response.append(match)
243
                    else:
244 1
                        response = match
245 1
                        break
246
        except AttributeError:
247
            return None
248 1
        if not many and isinstance(response, list):
249 1
            return None
250 1
        return response
251
252 1
    def process_tables(self, switch, table_id, args, stored_flows, actions):
253
        """Resolve the table context and get actions in the matched flow"""
254 1
        goto_table = False
255 1
        actions_ = []
256 1
        flow = self.match_flows(switch, table_id, args, stored_flows, False)
257 1
        if flow and 'actions' in flow['flow']:
258 1
            actions_ = flow['flow']['actions']
259 1
        elif flow and 'instructions' in flow['flow']:
260 1
            for instruction in flow['flow']['instructions']:
261 1
                if instruction['instruction_type'] == 'apply_actions':
262 1
                    actions_ = instruction['actions']
263 1
                elif instruction['instruction_type'] == 'goto_table':
264 1
                    table_id_ = instruction['table_id']
265 1
                    if table_id < table_id_:
266 1
                        table_id = table_id_
267 1
                        goto_table = True
268
                    else:
269 1
                        log.error(f"A packet can only been directed to a \
270
                                  flow table number greather than {table_id}")
271 1
                        raise ValueError('Wrong table_id')
272 1
        actions.extend(actions_)
273 1
        return flow, actions, goto_table, table_id
274
275 1
    def match_and_apply(self, switch, args, stored_flows):
276
        """Match flows and apply actions.
277
        Match given packet (in args) against
278
        the stored flows (from flow_manager) and,
279
        if a match flow is found, apply its actions."""
280 1
        table_id = 0
281 1
        goto_table = True
282 1
        port = None
283 1
        actions = []
284 1
        while goto_table:
285 1
            flow, actions, goto_table, table_id = self.process_tables(
286
                    switch, table_id, args, stored_flows, actions)
287 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 284 is not entered. Are you sure this can never be the case?
Loading history...
288 1
            return flow, args, port
289
290 1
        for action in actions:
291 1
            action_type = action['action_type']
292 1
            if action_type == 'output':
293 1
                port = action['port']
294 1
            if action_type == 'push_vlan':
295 1
                if 'dl_vlan' not in args:
296
                    args['dl_vlan'] = []
297 1
                args['dl_vlan'].append(0)
298 1
            if action_type == 'pop_vlan':
299 1
                if 'dl_vlan' in args:
300 1
                    args['dl_vlan'].pop()
301 1
                    if len(args['dl_vlan']) == 0:
302 1
                        del args['dl_vlan']
303 1
            if action_type == 'set_vlan':
304 1
                args['dl_vlan'][-1] = action['vlan_id']
305
        return flow, args, port
306