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