1
|
|
|
"""Automate circuit traces.""" |
2
|
|
|
|
3
|
1 |
|
import time |
4
|
|
|
|
5
|
1 |
|
import requests |
6
|
1 |
|
from kytos.core import KytosEvent, log |
7
|
1 |
|
from napps.amlight.sdntrace_cp import settings |
8
|
1 |
|
from napps.amlight.sdntrace_cp.scheduler import Scheduler |
9
|
1 |
|
from napps.amlight.sdntrace_cp.utils import clean_circuits, format_result |
10
|
1 |
|
from pyof.v0x04.common.port import PortNo as Port13 |
11
|
|
|
|
12
|
|
|
|
13
|
1 |
|
class Automate: |
14
|
|
|
"""Find all circuits and automate trace execution.""" |
15
|
|
|
|
16
|
1 |
|
def __init__(self, tracer): |
17
|
1 |
|
self._tracer = tracer |
18
|
1 |
|
self._circuits = [] |
19
|
1 |
|
self.find_circuits() |
20
|
1 |
|
self.ids = set() |
21
|
1 |
|
self.scheduler = Scheduler() |
22
|
|
|
|
23
|
|
|
# pylint: disable=too-many-nested-blocks, too-many-branches |
24
|
1 |
|
def find_circuits(self): |
25
|
|
|
"""Discover all circuits in a topology. |
26
|
|
|
|
27
|
|
|
Using the list of flows per switch, run control plane |
28
|
|
|
traces to find a list of circuits.""" |
29
|
1 |
|
all_flows = {} |
30
|
1 |
|
circuits = [] |
31
|
|
|
|
32
|
1 |
|
for switch in self._tracer.controller.switches.copy().values(): |
33
|
1 |
|
all_flows[switch] = [] |
34
|
1 |
|
if switch.ofp_version == '0x04': |
35
|
1 |
|
controller_port = Port13.OFPP_CONTROLLER |
36
|
|
|
|
37
|
1 |
|
try: |
38
|
1 |
|
for flow in switch.generic_flows: |
39
|
1 |
|
action_ok = False |
40
|
1 |
|
in_port_ok = False |
41
|
1 |
|
if 'in_port' in flow.match and flow.match['in_port'] != 0: |
42
|
1 |
|
in_port_ok = True |
43
|
1 |
|
if in_port_ok: |
44
|
1 |
|
for action in flow.actions: |
45
|
1 |
|
if action.action_type == 'output' \ |
46
|
|
|
and action.port != controller_port: |
|
|
|
|
47
|
1 |
|
action_ok = True |
48
|
1 |
|
if action_ok: |
49
|
1 |
|
all_flows[switch].append(flow) |
50
|
|
|
except AttributeError: |
51
|
|
|
pass |
52
|
|
|
|
53
|
1 |
|
for switch, flows in all_flows.items(): |
54
|
1 |
|
for flow in flows: |
55
|
1 |
|
in_port = flow.match['in_port'] |
56
|
1 |
|
vlan = None |
57
|
1 |
|
if 'vlan_vid' in flow.match: |
58
|
|
|
vlan = flow.match['vlan_vid'] |
59
|
1 |
|
if switch.ofp_version == '0x04': |
60
|
1 |
|
in_port = in_port.value |
61
|
1 |
|
if vlan: |
62
|
|
|
vlan = vlan.value |
63
|
1 |
|
entries = { |
64
|
|
|
'trace': { |
65
|
|
|
'switch': { |
66
|
|
|
'dpid': switch.dpid, |
67
|
|
|
'in_port': in_port |
68
|
|
|
}, |
69
|
|
|
'eth': { |
70
|
|
|
'dl_vlan': vlan |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
} |
74
|
1 |
|
result = self._tracer.tracepath(entries) |
75
|
1 |
|
circuits.append({'circuit': format_result(result), |
76
|
|
|
'entries': entries}) |
77
|
|
|
|
78
|
1 |
|
self._circuits = clean_circuits(circuits, self._tracer.controller) |
79
|
|
|
|
80
|
1 |
|
def run_traces(self): |
81
|
|
|
"""Run traces for all circuits.""" |
82
|
|
|
|
83
|
1 |
|
results = [] |
84
|
1 |
|
for circuit in self._circuits: |
85
|
1 |
|
entries = circuit['entries'] |
86
|
1 |
|
result = self._tracer.tracepath(entries) |
87
|
1 |
|
try: |
88
|
1 |
|
result = format_result(result) |
89
|
1 |
|
if result != circuit['circuit']: |
90
|
1 |
|
results.append(circuit) |
91
|
|
|
except KeyError: |
92
|
|
|
results.append(circuit) |
93
|
1 |
|
log.debug('Results %s, size %s', results, len(self._circuits)) |
94
|
1 |
|
return results |
95
|
|
|
|
96
|
1 |
|
def get_circuit(self, circuit): |
97
|
|
|
"""Find the given circuit in the list of circuits.""" |
98
|
1 |
|
for steps in self._circuits: |
99
|
1 |
|
endpoint_a = steps['circuit'][0] |
100
|
1 |
|
endpoint_z = steps['circuit'][-1] |
101
|
|
|
# pylint: disable=too-many-boolean-expressions |
102
|
1 |
|
if (circuit['dpid_a'] == endpoint_a['dpid'] and |
103
|
|
|
circuit['port_a'] == endpoint_a['in_port'] and |
104
|
|
|
circuit['vlan_a'] == endpoint_a['in_vlan'] and |
105
|
|
|
circuit['dpid_z'] == endpoint_z['dpid'] and |
106
|
|
|
circuit['port_z'] == endpoint_z['out_port'] and |
107
|
|
|
circuit['vlan_z'] == endpoint_z['out_vlan']): |
108
|
|
|
|
109
|
1 |
|
return steps['circuit'] |
110
|
1 |
|
return None |
111
|
|
|
|
112
|
1 |
|
def _check_trace(self, circuit, trace): |
113
|
1 |
|
steps = self.get_circuit(circuit) |
114
|
1 |
|
if steps: |
115
|
1 |
|
if len(steps) != len(trace) - 1: |
116
|
1 |
|
return False |
117
|
1 |
|
for i, step in enumerate(steps): |
118
|
1 |
|
if not self.check_step(step, trace[i]): |
119
|
1 |
|
return False |
120
|
|
|
else: |
121
|
1 |
|
return False |
122
|
1 |
|
return True |
123
|
|
|
|
124
|
1 |
|
def run_important_traces(self): |
125
|
|
|
"""Run SDNTrace in data plane for important circuits as defined |
126
|
|
|
by user.""" |
127
|
1 |
|
event = KytosEvent(name='amlight/kytos_courier.slack_send') |
128
|
1 |
|
content = { |
129
|
|
|
'channel': settings.SLACK_CHANNEL, |
130
|
|
|
'source': 'amlight/sdntrace_cp' |
131
|
|
|
} |
132
|
|
|
|
133
|
1 |
|
try: |
134
|
1 |
|
important_circuits = settings.IMPORTANT_CIRCUITS |
135
|
|
|
except AttributeError: |
136
|
|
|
return |
137
|
|
|
|
138
|
1 |
|
for circuit in important_circuits: |
139
|
1 |
|
entries = { |
140
|
|
|
'trace': { |
141
|
|
|
'switch': { |
142
|
|
|
'dpid': circuit['dpid_a'], |
143
|
|
|
'in_port': circuit['port_a'] |
144
|
|
|
}, |
145
|
|
|
'eth': { |
146
|
|
|
'dl_vlan': circuit['vlan_a'] |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
} |
150
|
1 |
|
result = requests.put(settings.SDNTRACE_URL, json=entries) |
151
|
1 |
|
trace = result.json() |
152
|
1 |
|
trace_id = trace['result']['trace_id'] |
153
|
1 |
|
step_type = None |
154
|
1 |
|
while step_type != 'last': |
155
|
1 |
|
time.sleep(5) |
156
|
1 |
|
result = requests.get(f'{settings.SDNTRACE_URL}/{trace_id}') |
157
|
1 |
|
trace = result.json() |
158
|
1 |
|
step_type = trace['result'][-1]['type'] |
159
|
1 |
|
check = self._check_trace(circuit, trace['result']) |
160
|
1 |
|
if check is False: |
161
|
1 |
|
content['m_body'] = 'Trace in data plane different from ' \ |
162
|
|
|
'trace in control plane for circuit ' \ |
163
|
|
|
f'{circuit}' |
164
|
1 |
|
event.content['message'] = content |
165
|
1 |
|
self._tracer.controller.buffers.app.put(event) |
166
|
|
|
|
167
|
1 |
|
def schedule_id(self, id_): |
168
|
|
|
"""Keep track of scheduled ids""" |
169
|
1 |
|
if not isinstance(id_, str): |
170
|
1 |
|
raise AttributeError("Invalid id type, for schedule.") |
171
|
1 |
|
self.ids.add(id_) |
172
|
1 |
|
return id_ |
173
|
|
|
|
174
|
1 |
|
def schedule_traces(self, settings_=settings): |
175
|
|
|
"""Check for invalid arguments from schedule""" |
176
|
1 |
|
if settings_.TRIGGER_SCHEDULE_TRACES: |
177
|
1 |
|
id_ = self.schedule_id('automatic_traces') |
178
|
1 |
|
trigger = settings_.SCHEDULE_TRIGGER |
179
|
1 |
|
kwargs = settings_.SCHEDULE_ARGS |
180
|
1 |
|
if (not isinstance(kwargs, dict) or |
181
|
|
|
not isinstance(trigger, str)): |
182
|
1 |
|
raise AttributeError("Invalid attributes for " |
183
|
|
|
"job to be scheduled.") |
184
|
1 |
|
trigger_args = {} |
185
|
1 |
|
trigger_args['kwargs'] = {'trigger': trigger} |
186
|
1 |
|
trigger_args['kwargs'].update(kwargs) |
187
|
1 |
|
self.scheduler.add_callable(id_, self.run_traces, |
188
|
|
|
**trigger_args['kwargs']) |
189
|
1 |
|
return self.scheduler.get_job(id_) |
190
|
1 |
|
return None |
191
|
|
|
|
192
|
1 |
|
def schedule_important_traces(self, settings_=settings): |
193
|
|
|
"""Check for invalid important arguments from schedule""" |
194
|
1 |
|
if settings_.TRIGGER_IMPORTANT_CIRCUITS: |
195
|
1 |
|
id_ = self.schedule_id('automatic_important_traces') |
196
|
1 |
|
trigger = settings_.IMPORTANT_CIRCUITS_TRIGGER |
197
|
1 |
|
kwargs = settings_.IMPORTANT_CIRCUITS_ARGS |
198
|
1 |
|
if (not isinstance(kwargs, dict) or |
199
|
|
|
not isinstance(trigger, str)): |
200
|
1 |
|
raise AttributeError("Invalid attributes for " |
201
|
|
|
"job to be scheduled.") |
202
|
1 |
|
trigger_args = {} |
203
|
1 |
|
trigger_args['kwargs'] = {'trigger': trigger} |
204
|
1 |
|
trigger_args['kwargs'].update(kwargs) |
205
|
1 |
|
return self.scheduler.add_callable(id_, |
206
|
|
|
self.run_important_traces, |
207
|
|
|
**trigger_args['kwargs']) |
208
|
1 |
|
return None |
209
|
|
|
|
210
|
1 |
|
def unschedule_ids(self, id_set=None): |
211
|
|
|
"""Remove ids to be unschedule""" |
212
|
1 |
|
if id_set is None: |
213
|
1 |
|
id_set = self.ids.copy() |
214
|
1 |
|
while id_set: |
215
|
1 |
|
id_ = id_set.pop() |
216
|
1 |
|
self.ids.discard(id_) |
217
|
1 |
|
self.scheduler.remove_job(id_) |
218
|
|
|
|
219
|
1 |
|
def sheduler_shutdown(self, wait): |
220
|
|
|
"""Shutdown scheduler""" |
221
|
|
|
self.scheduler.shutdown(wait) |
222
|
|
|
|
223
|
1 |
|
@staticmethod |
224
|
1 |
|
def check_step(circuit_step, trace_step): |
225
|
|
|
"""Check if a step in SDNTrace in data plane is what it should""" |
226
|
1 |
|
return (circuit_step['dpid'] == trace_step['dpid'] and |
227
|
|
|
circuit_step['in_port'] == trace_step['port']) |
228
|
|
|
|