Passed
Pull Request — master (#47)
by
unknown
02:56
created

build.main   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Test Coverage

Coverage 67.48%

Importance

Changes 0
Metric Value
eloc 179
dl 0
loc 262
rs 5.04
c 0
b 0
f 0
ccs 110
cts 163
cp 0.6748
wmc 57

13 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.update_circuits() 0 6 2
A Main.trace_step() 0 18 4
A Main.get_stored_flows() 0 15 5
A Main.shutdown() 0 7 1
A Main.setup() 0 14 1
A Main.execute() 0 2 1
A Main.has_loop() 0 8 4
C Main.match_and_apply() 0 29 11
B Main.tracepath() 0 42 7
A Main.get_traces() 0 17 4
A Main.trace() 0 7 1
B Main.match_flows() 0 27 7
C Main.do_match() 0 23 9

How to fix   Complexity   

Complexity

Complex classes like build.main 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 ipaddress
7 1
from datetime import datetime
8
9 1
import requests
10 1
from flask import jsonify, request
11 1
from kytos.core import KytosNApp, log, rest
12 1
from kytos.core.helpers import listen_to
13 1
from napps.amlight.sdntrace_cp import settings
14 1
from napps.amlight.sdntrace_cp.automate import Automate
15 1
from napps.amlight.sdntrace_cp.utils import (convert_entries,
16
                                             convert_list_entries,
17
                                             find_endpoint, prepare_json,
18
                                             prepare_list_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
    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
        self.traces = {}
38 1
        self.last_id = 30000
39 1
        self.automate = Automate(self)
40 1
        self.automate.schedule_traces()
41 1
        self.automate.schedule_important_traces()
42
43 1
    def execute(self):
44
        """This method is executed right after the setup method execution.
45
46
        You can also use this method in loop mode if you add to the above setup
47
        method a line like the following example:
48
49
            self.execute_as_loop(30)  # 30-second interval.
50
        """
51
52 1
    def shutdown(self):
53
        """This method is executed when your napp is unloaded.
54
55
        If you have some cleanup procedure, insert it here.
56
        """
57
        self.automate.unschedule_ids()
58
        self.automate.sheduler_shutdown(wait=False)
59
60 1
    @rest('/trace', methods=['PUT'])
61 1
    def trace(self):
62
        """Trace a path."""
63 1
        entries = request.get_json()
64 1
        entries = convert_entries(entries)
65 1
        result = self.tracepath(entries)
66 1
        return jsonify(prepare_json(result))
67
68 1
    @rest('/traces', methods=['PUT'])
69 1
    def get_traces(self):
70
        """For bulk requests."""
71 1
        entries = request.get_json()
72 1
        entries = convert_list_entries(entries)
73 1
        results = {}
74 1
        list_ready = []
75 1
        for entry in entries:
76 1
            if (entry['dpid'], entry['in_port']) in list_ready:
77 1
                continue
78 1
            list_ready.append((entry['dpid'], entry['in_port']))
79 1
            dpid = entry['dpid']
80 1
            if dpid not in results:
81 1
                results[dpid] = []
82 1
            result = prepare_list_json(self.tracepath(entry))
83 1
            results[dpid].append(result)
84 1
        return jsonify(results)
85
86 1
    def tracepath(self, entries):
87
        """Trace a path for a packet represented by entries."""
88 1
        self.last_id += 1
89 1
        trace_id = self.last_id
90 1
        trace_result = []
91 1
        trace_type = 'starting'
92 1
        do_trace = True
93 1
        while do_trace:
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 1
            switch = self.controller.get_switch_by_dpid(entries['dpid'])
101 1
            result = self.trace_step(switch, entries)
102 1
            if result:
103 1
                out = {'port': result['out_port']}
104 1
                if 'dl_vlan' in result['entries']:
105
                    out.update({'vlan': result['entries']['dl_vlan'][-1]})
106 1
                trace_step.update({
107
                    'out': out
108
                })
109 1
                if 'dpid' in result:
110 1
                    next_step = {'dpid': result['dpid'],
111
                                 'port': result['in_port']}
112 1
                    if self.has_loop(next_step, trace_result):
113 1
                        do_trace = False
114
                    else:
115 1
                        entries = result['entries']
116 1
                        entries['dpid'] = result['dpid']
117 1
                        entries['in_port'] = result['in_port']
118 1
                        trace_type = 'trace'
119
                else:
120
                    do_trace = False
121
            else:
122 1
                do_trace = False
123 1
            trace_result.append(trace_step)
124 1
        self.traces.update({
125
            trace_id: trace_result
126
        })
127 1
        return trace_result
128
129 1
    @staticmethod
130 1
    def has_loop(trace_step, trace_result):
131
        """Check if there is a loop in the trace result."""
132 1
        for trace in trace_result:
133 1
            if trace['in']['dpid'] == trace_step['dpid'] and \
134
                            trace['in']['port'] == trace_step['port']:
135 1
                return True
136 1
        return False
137
138 1
    def trace_step(self, switch, entries):
139
        """Perform a trace step.
140
141
        Match the given fields against the switch's list of flows."""
142 1
        flow, entries, port = self.match_and_apply(switch, entries)
143
144 1
        if not flow or not port:
145 1
            return None
146
147 1
        endpoint = find_endpoint(switch, port)
148 1
        if endpoint is None:
149 1
            return {'out_port': port,
150
                    'entries': entries}
151
152 1
        return {'dpid': endpoint.switch.dpid,
153
                'in_port': endpoint.port_number,
154
                'out_port': port,
155
                'entries': entries}
156
157 1
    @listen_to('amlight/flow_stats.flows_updated')
158 1
    def update_circuits(self, event):
159
        """Update the list of circuits after a flow change."""
160
        # pylint: disable=unused-argument
161 1
        if settings.FIND_CIRCUITS_IN_FLOWS:
162 1
            self.automate.find_circuits()
163
164 1
    @staticmethod
165 1
    def get_stored_flows(dpids: list = None, state: str = None):
166
        """Get stored flows from flow_manager napps."""
167
        api_url = f'{settings.FLOW_MANAGER_URL}/stored_flows'
168
        if dpids:
169
            str_dpids = ''
170
            for dpid in dpids:
171
                str_dpids += f'&dpid={dpid}'
172
            api_url += '/?'+str_dpids[1:]
173
        if state:
174
            char = '&' if dpids else '/?'
175
            api_url += char+f'state={state}'
176
        result = requests.get(api_url)
177
        flows_from_manager = result.json()
178
        return flows_from_manager
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 1
        if ('match' not in flow['flow']) or (len(flow['flow']['match']) == 0):
185 1
            return False
186
        for name in flow['flow']['match']:
187
            field_flow = flow['flow']['match'][name]
188
            if name not in args:
189
                return False
190
            if name == 'dl_vlan':
191
                field = args[name][-1]
192
            else:
193
                field = args[name]
194
            if name not in ('ipv4_src', 'ipv4_dst', 'ipv6_src', 'ipv6_dst'):
195
                if field_flow != field:
196
                    return False
197
            else:
198
                packet_ip = int(ipaddress.ip_address(field))
199
                ip_addr = flow['flow']['match'][name]
200
                if packet_ip & ip_addr.netmask != ip_addr.address:
201
                    return False
202
        return flow
203
204 1
    def match_flows(self, switch, args, many=True):
205
        # pylint: disable=bad-staticmethod-argument
206
        """
207
        Match the packet in request against the stored flows from flow_manager.
208
        Try the match with each flow, in other. If many is True, tries the
209
        match with all flows, if False, tries until the first match.
210
        :param args: packet data
211
        :param many: Boolean, indicating whether to continue after matching the
212
                first flow or not
213
        :return: If many, the list of matched flows, or the matched flow
214
        """
215 1
        stored_flows = self.get_stored_flows()
216 1
        response = []
217 1
        try:
218 1
            for flow in stored_flows[switch.dpid]:
219 1
                match = Main.do_match(flow, args)
220 1
                if match:
221
                    if many:
222
                        response.append(match)
223
                    else:
224
                        response = match
225
                        break
226 1
        except AttributeError:
227 1
            return None
228 1
        if not many and isinstance(response, list):
229 1
            return None
230
        return response
231
232
    # pylint: disable=redefined-outer-name
233 1
    def match_and_apply(self, switch, args):
234
        # pylint: disable=bad-staticmethod-argument
235
        """Match flows and apply actions.
236
        Match given packet (in args) against
237
        the stored flows (from flow_manager) and,
238
        if a match flow is found, apply its actions."""
239 1
        flow = self.match_flows(switch, args, False)
240 1
        port = None
241 1
        actions = None
242
        # pylint: disable=too-many-nested-blocks
243 1
        if not flow or switch.ofp_version != '0x04':
244 1
            return flow, args, port
245
        actions = flow['flow']['actions']
246
        for action in actions:
247
            action_type = action['action_type']
248
            if action_type == 'output':
249
                port = action['port']
250
            if action_type == 'push_vlan':
251
                if 'dl_vlan' not in args:
252
                    args['dl_vlan'] = []
253
                args['dl_vlan'].append(0)
254
            if action_type == 'pop_vlan':
255
                if 'dl_vlan' in args:
256
                    args['dl_vlan'].pop()
257
                    if len(args['dl_vlan']) == 0:
258
                        del args['dl_vlan']
259
            if action_type == 'set_vlan':
260
                args['dl_vlan'][-1] = action['vlan_id']
261
        return flow, args, port
262