Passed
Pull Request — master (#95)
by
unknown
02:53
created

build.main.Main.update_circuits()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 ipaddress
7 1
import pathlib
8 1
from datetime import datetime
9
10 1
from flask import jsonify
11 1
from kytos.core import KytosNApp, log, rest
12 1
from kytos.core.helpers import load_spec, validate_openapi
13 1
from napps.amlight.sdntrace_cp.utils import (convert_entries,
14
                                             convert_list_entries,
15
                                             find_endpoint, get_stored_flows,
16
                                             match_field_dl_vlan, prepare_json)
17
18
19 1
class Main(KytosNApp):
20
    """Main class of amlight/sdntrace_cp NApp.
21
22
    This application gets the list of flows from the switches
23
    and uses it to trace paths without using the data plane.
24
    """
25
26 1
    spec = load_spec(pathlib.Path(__file__).parent / "openapi.yml")
27
28 1
    def setup(self):
29
        """Replace the '__init__' method for the KytosNApp subclass.
30
31
        The setup method is automatically called by the controller when your
32
        application is loaded.
33
34
        """
35 1
        log.info("Starting Kytos SDNTrace CP App!")
36
37 1
    def execute(self):
38
        """This method is executed right after the setup method execution.
39
40
        You can also use this method in loop mode if you add to the above setup
41
        method a line like the following example:
42
43
            self.execute_as_loop(30)  # 30-second interval.
44
        """
45
46 1
    def shutdown(self):
47
        """This method is executed when your napp is unloaded.
48
49
        If you have some cleanup procedure, insert it here.
50
        """
51
52 1
    @rest('/v1/trace', methods=['PUT'])
53 1
    @validate_openapi(spec)
54 1
    def trace(self, data):
55
        """Trace a path."""
56 1
        result = []
57 1
        entries = convert_entries(data)
58 1
        if not entries:
59
            return "Bad request", 400
60 1
        stored_flows = get_stored_flows()
61 1
        result = self.tracepath(entries, stored_flows)
62 1
        return jsonify(prepare_json(result))
63
64 1
    @rest('/v1/traces', methods=['PUT'])
65 1
    @validate_openapi(spec)
66 1
    def get_traces(self, data):
67
        """For bulk requests."""
68 1
        entries = convert_list_entries(data)
69 1
        stored_flows = get_stored_flows()
70 1
        results = []
71 1
        for entry in entries:
72 1
            results.append(self.tracepath(entry, stored_flows))
73 1
        temp = prepare_json(results)
74 1
        return jsonify(temp)
75
76 1
    def tracepath(self, entries, stored_flows):
77
        """Trace a path for a packet represented by entries."""
78
        # pylint: disable=too-many-branches
79 1
        trace_result = []
80 1
        trace_type = 'starting'
81 1
        do_trace = True
82 1
        while do_trace:
83 1
            if 'dpid' not in entries or 'in_port' not in entries:
84
                break
85 1
            trace_step = {'in': {'dpid': entries['dpid'],
86
                                 'port': entries['in_port'],
87
                                 'time': str(datetime.now()),
88
                                 'type': trace_type}}
89 1
            if 'dl_vlan' in entries:
90 1
                trace_step['in'].update({'vlan': entries['dl_vlan'][-1]})
91
92 1
            switch = self.controller.get_switch_by_dpid(entries['dpid'])
93 1
            if not switch:
94 1
                trace_step['in']['type'] = 'last'
95 1
                trace_result.append(trace_step)
96 1
                break
97 1
            result = self.trace_step(switch, entries, stored_flows)
98 1
            if result:
99 1
                out = {'port': result['out_port']}
100 1
                if 'dl_vlan' in result['entries']:
101 1
                    out.update({'vlan': result['entries']['dl_vlan'][-1]})
102 1
                trace_step.update({
103
                    'out': out
104
                })
105 1
                if 'dpid' in result:
106 1
                    next_step = {'dpid': result['dpid'],
107
                                 'port': result['in_port']}
108 1
                    entries = result['entries']
109 1
                    entries['dpid'] = result['dpid']
110 1
                    entries['in_port'] = result['in_port']
111 1
                    if self.has_loop(next_step, trace_result):
112 1
                        trace_step['in']['type'] = 'loop'
113 1
                        do_trace = False
114
                    else:
115 1
                        trace_type = 'intermediary'
116
                else:
117 1
                    trace_step['in']['type'] = 'last'
118 1
                    do_trace = False
119
            else:
120 1
                trace_step['in']['type'] = 'incomplete'
121 1
                do_trace = False
122 1
            if 'out' in trace_step and trace_step['out']:
123 1
                if self.check_loop_trace_step(trace_step, trace_result):
124 1
                    do_trace = False
125 1
            trace_result.append(trace_step)
126 1
        return trace_result
127
128 1
    @staticmethod
129 1
    def check_loop_trace_step(trace_step, trace_result):
130
        """Check if there is a loop in the trace and add the step."""
131
        # outgoing interface is the same as the input interface
132 1
        if not trace_result and \
133
                trace_step['in']['type'] == 'last' and \
134
                trace_step['in']['port'] == trace_step['out']['port']:
135 1
            trace_step['in']['type'] = 'loop'
136 1
            return True
137 1
        if trace_result and \
138
                trace_result[0]['in']['dpid'] == trace_step['in']['dpid'] and \
139
                trace_result[0]['in']['port'] == trace_step['out']['port']:
140 1
            trace_step['in']['type'] = 'loop'
141 1
            return True
142 1
        return False
143
144 1
    @staticmethod
145 1
    def has_loop(trace_step, trace_result):
146
        """Check if there is a loop in the trace result."""
147 1
        for trace in trace_result:
148 1
            if trace['in']['dpid'] == trace_step['dpid'] and \
149
                            trace['in']['port'] == trace_step['port']:
150 1
                return True
151 1
        return False
152
153 1
    def trace_step(self, switch, entries, stored_flows):
154
        """Perform a trace step.
155
156
        Match the given fields against the switch's list of flows."""
157 1
        flow, entries, port = self.match_and_apply(
158
                                                    switch,
159
                                                    entries,
160
                                                    stored_flows
161
                                                )
162
163 1
        if not flow or not port:
164 1
            return None
165
166 1
        endpoint = find_endpoint(switch, port)
167 1
        if endpoint is None:
168
            log.warning(f"Port {port} not found on switch {switch}")
169
            return None
170 1
        endpoint = endpoint['endpoint']
171 1
        if endpoint is None:
172 1
            return {'out_port': port,
173
                    'entries': entries}
174
175 1
        return {'dpid': endpoint.switch.dpid,
176
                'in_port': endpoint.port_number,
177
                'out_port': port,
178
                'entries': entries}
179
180 1
    @classmethod
181 1
    def do_match(cls, flow, args):
182
        """Match a packet against this flow (OF1.3)."""
183
        # pylint: disable=consider-using-dict-items
184
        # pylint: disable=too-many-return-statements
185 1
        if ('match' not in flow['flow']) or (len(flow['flow']['match']) == 0):
186 1
            return False
187 1
        for name in flow['flow']['match']:
188 1
            field_flow = flow['flow']['match'][name]
189 1
            if name == 'dl_vlan':
190 1
                field = args.get(name)
191 1
                if field:
192 1
                    field = field[-1]
193 1
                if not match_field_dl_vlan(field, field_flow):
194 1
                    return False
195
                continue
196 1
            if name not in args:
197
                return False
198 1
            field = args[name]
199 1
            if name not in ('ipv4_src', 'ipv4_dst', 'ipv6_src', 'ipv6_dst'):
200 1
                if field_flow != field:
201 1
                    return False
202
            else:
203
                packet_ip = int(ipaddress.ip_address(field))
204
                ip_addr = flow['flow']['match'][name]
205
                if packet_ip & ip_addr.netmask != ip_addr.address:
206
                    return False
207 1
        return flow
208
209 1
    def match_flows(self, switch, args, stored_flows, many=True):
210
        # pylint: disable=bad-staticmethod-argument
211
        """
212
        Match the packet in request against the stored flows from flow_manager.
213
        Try the match with each flow, in other. If many is True, tries the
214
        match with all flows, if False, tries until the first match.
215
        :param args: packet data
216
        :param many: Boolean, indicating whether to continue after matching the
217
                first flow or not
218
        :return: If many, the list of matched flows, or the matched flow
219
        """
220 1
        if switch.dpid not in stored_flows:
221
            return None
222 1
        response = []
223 1
        if switch.dpid not in stored_flows:
224
            return None
225 1
        try:
226 1
            for flow in stored_flows[switch.dpid]:
227 1
                match = Main.do_match(flow, args)
228 1
                if match:
229 1
                    if many:
230
                        response.append(match)
231
                    else:
232 1
                        response = match
233 1
                        break
234
        except AttributeError:
235
            return None
236 1
        if not many and isinstance(response, list):
237 1
            return None
238 1
        return response
239
240
    # pylint: disable=redefined-outer-name
241 1
    def match_and_apply(self, switch, args, stored_flows):
242
        # pylint: disable=bad-staticmethod-argument
243
        """Match flows and apply actions.
244
        Match given packet (in args) against
245
        the stored flows (from flow_manager) and,
246
        if a match flow is found, apply its actions."""
247 1
        flow = self.match_flows(switch, args, stored_flows, False)
248 1
        port = None
249 1
        actions = []
250
        # pylint: disable=too-many-nested-blocks
251 1
        if not flow or switch.ofp_version != '0x04':
252 1
            return flow, args, port
253 1
        if 'actions' in flow['flow']:
254 1
            actions = flow['flow']['actions']
255 1
        for action in actions:
256 1
            action_type = action['action_type']
257 1
            if action_type == 'output':
258 1
                port = action['port']
259 1
            if action_type == 'push_vlan':
260 1
                if 'dl_vlan' not in args:
261
                    args['dl_vlan'] = []
262 1
                args['dl_vlan'].append(0)
263 1
            if action_type == 'pop_vlan':
264 1
                if 'dl_vlan' in args:
265 1
                    args['dl_vlan'].pop()
266 1
                    if len(args['dl_vlan']) == 0:
267 1
                        del args['dl_vlan']
268 1
            if action_type == 'set_vlan':
269 1
                args['dl_vlan'][-1] = action['vlan_id']
270
        return flow, args, port
271