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.discard(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
|
|
|
|