1
|
|
|
"""High-level abstraction for Flows of multiple OpenFlow versions. |
2
|
|
|
|
3
|
|
|
Use common fields of FlowStats/FlowMod of supported OF versions. ``match`` and |
4
|
|
|
``actions`` fields are different, so Flow, Action and Match related classes are |
5
|
|
|
inherited in v0x01 and v0x04 modules. |
6
|
|
|
""" |
7
|
1 |
|
import json |
8
|
1 |
|
from abc import ABC, abstractmethod |
9
|
1 |
|
from hashlib import md5 |
10
|
|
|
|
11
|
|
|
# Note: FlowModCommand is the same in both v0x01 and v0x04 |
12
|
1 |
|
from pyof.v0x04.controller2switch.flow_mod import FlowModCommand |
13
|
|
|
|
14
|
1 |
|
from napps.kytos.of_core import v0x01, v0x04 |
15
|
|
|
|
16
|
|
|
|
17
|
1 |
|
class FlowFactory(ABC): # pylint: disable=too-few-public-methods |
18
|
|
|
"""Choose the correct Flow according to OpenFlow version.""" |
19
|
|
|
|
20
|
1 |
|
@classmethod |
21
|
|
|
def from_of_flow_stats(cls, of_flow_stats, switch): |
22
|
|
|
"""Return a Flow for the switch OpenFlow version.""" |
23
|
|
|
flow_class = cls.get_class(switch) |
24
|
|
|
return flow_class.from_of_flow_stats(of_flow_stats, switch) |
25
|
|
|
|
26
|
1 |
|
@staticmethod |
27
|
|
|
def get_class(switch): |
28
|
|
|
"""Return the Flow class for the switch OF version.""" |
29
|
|
|
of_version = switch.connection.protocol.version |
30
|
|
|
if of_version == 0x01: |
31
|
|
|
return v0x01.flow.Flow |
32
|
|
|
if of_version == 0x04: |
33
|
|
|
return v0x04.flow.Flow |
34
|
|
|
raise NotImplementedError(f'Unsupported OpenFlow version {of_version}') |
35
|
|
|
|
36
|
|
|
|
37
|
1 |
|
class FlowBase(ABC): # pylint: disable=too-many-instance-attributes |
38
|
|
|
"""Class to abstract a Flow to switches. |
39
|
|
|
|
40
|
|
|
This class represents a Flow installed or to be installed inside the |
41
|
|
|
switch. A flow, in this case is represented by a Match object and a set of |
42
|
|
|
actions that should occur in case any match happen. |
43
|
|
|
""" |
44
|
|
|
|
45
|
|
|
# of_version number: 0x01, 0x04 |
46
|
1 |
|
of_version = None |
47
|
|
|
|
48
|
|
|
# Subclasses must set their version-specific classes |
49
|
1 |
|
_action_factory = None |
50
|
1 |
|
_flow_mod_class = None |
51
|
1 |
|
_match_class = None |
52
|
|
|
|
53
|
1 |
|
def __init__(self, switch, table_id=0xff, match=None, priority=0x8000, |
54
|
|
|
idle_timeout=0, hard_timeout=0, cookie=0, actions=None, |
55
|
|
|
stats=None): |
56
|
|
|
"""Assign parameters to attributes. |
57
|
|
|
|
58
|
|
|
Args: |
59
|
|
|
switch (kytos.core.switch.Switch): Switch ID is used to uniquely |
60
|
|
|
identify a flow. |
61
|
|
|
table_id (int): The index of a single table or 0xff for all tables. |
62
|
|
|
match (|match|): Match object. |
63
|
|
|
priority (int): Priority level of flow entry. |
64
|
|
|
idle_timeout (int): Idle time before discarding, in seconds. |
65
|
|
|
hard_timeout (int): Max time before discarding, in seconds. |
66
|
|
|
cookie (int): Opaque controller-issued identifier. |
67
|
|
|
actions (|list_of_actions|): List of actions to apply. |
68
|
|
|
stats (Stats): Latest flow statistics. |
69
|
|
|
""" |
70
|
|
|
# pylint: disable=too-many-arguments,too-many-locals |
71
|
1 |
|
self.switch = switch |
72
|
1 |
|
self.table_id = table_id |
73
|
|
|
# Disable not-callable error as subclasses set a class |
74
|
1 |
|
self.match = match or self._match_class() # pylint: disable=E1102 |
75
|
1 |
|
self.priority = priority |
76
|
1 |
|
self.idle_timeout = idle_timeout |
77
|
1 |
|
self.hard_timeout = hard_timeout |
78
|
1 |
|
self.cookie = cookie |
79
|
1 |
|
self.actions = actions or [] |
80
|
1 |
|
self.stats = stats or FlowStats() # pylint: disable=E1102 |
81
|
|
|
|
82
|
1 |
|
@property |
83
|
|
|
def id(self): # pylint: disable=invalid-name |
84
|
|
|
"""Return this flow unique identifier. |
85
|
|
|
|
86
|
|
|
Calculate an md5 hash based on this object's modified json string. The |
87
|
|
|
json for ID calculation excludes ``stats`` attribute that changes over |
88
|
|
|
time. |
89
|
|
|
|
90
|
|
|
Returns: |
91
|
|
|
str: Flow unique identifier (md5sum). |
92
|
|
|
|
93
|
|
|
""" |
94
|
1 |
|
flow_str = self.as_json(sort_keys=True, include_id=False) |
95
|
1 |
|
md5sum = md5() |
96
|
1 |
|
md5sum.update(flow_str.encode('utf-8')) |
97
|
1 |
|
return md5sum.hexdigest() |
98
|
|
|
|
99
|
1 |
|
def as_dict(self, include_id=True): |
100
|
|
|
"""Return the Flow as a serializable Python dictionary. |
101
|
|
|
|
102
|
|
|
Args: |
103
|
|
|
include_id (bool): Default is ``True``. Internally, it is set to |
104
|
|
|
``False`` when calculating the flow ID that is based in this |
105
|
|
|
dictionary's JSON string. |
106
|
|
|
|
107
|
|
|
Returns: |
108
|
|
|
dict: Serializable dictionary. |
109
|
|
|
|
110
|
|
|
""" |
111
|
1 |
|
flow_dict = { |
112
|
|
|
'switch': self.switch.id, |
113
|
|
|
'table_id': self.table_id, |
114
|
|
|
'match': self.match.as_dict(), |
115
|
|
|
'priority': self.priority, |
116
|
|
|
'idle_timeout': self.idle_timeout, |
117
|
|
|
'hard_timeout': self.hard_timeout, |
118
|
|
|
'cookie': self.cookie, |
119
|
|
|
'actions': [action.as_dict() for action in self.actions]} |
120
|
1 |
|
if include_id: |
121
|
|
|
# Avoid infinite recursion |
122
|
1 |
|
flow_dict['id'] = self.id |
123
|
|
|
# Remove statistics that change over time |
124
|
1 |
|
flow_dict['stats'] = self.stats.as_dict() |
125
|
|
|
|
126
|
1 |
|
return flow_dict |
127
|
|
|
|
128
|
1 |
|
@classmethod |
129
|
|
|
def from_dict(cls, flow_dict, switch): |
130
|
|
|
"""Return an instance with values from ``flow_dict``.""" |
131
|
1 |
|
flow = cls(switch) |
132
|
|
|
|
133
|
|
|
# Set attributes found in ``flow_dict`` |
134
|
1 |
|
for attr_name, attr_value in flow_dict.items(): |
135
|
1 |
|
if attr_name in vars(flow): |
136
|
1 |
|
setattr(flow, attr_name, attr_value) |
137
|
|
|
|
138
|
1 |
|
flow.switch = switch |
139
|
1 |
|
if 'stats' in flow_dict: |
140
|
1 |
|
flow.stats = FlowStats.from_dict(flow_dict['stats']) |
141
|
|
|
|
142
|
|
|
# Version-specific attributes |
143
|
1 |
|
if 'match' in flow_dict: |
144
|
1 |
|
flow.match = cls._match_class.from_dict(flow_dict['match']) |
145
|
1 |
|
if 'actions' in flow_dict: |
146
|
1 |
|
flow.actions = [] |
147
|
1 |
|
for action_dict in flow_dict['actions']: |
148
|
1 |
|
action = cls._action_factory.from_dict(action_dict) |
149
|
1 |
|
if action: |
150
|
1 |
|
flow.actions.append(action) |
151
|
|
|
|
152
|
1 |
|
return flow |
153
|
|
|
|
154
|
1 |
|
def as_json(self, sort_keys=False, include_id=True): |
155
|
|
|
"""Return the representation of a flow in JSON format. |
156
|
|
|
|
157
|
|
|
Args: |
158
|
|
|
sort_keys (bool): ``False`` by default (Python's default). Sorting |
159
|
|
|
is used, for example, to calculate the flow ID. |
160
|
|
|
include_id (bool): ``True`` by default. Internally, the ID is not |
161
|
|
|
included while calculating it. |
162
|
|
|
|
163
|
|
|
Returns: |
164
|
|
|
string: Flow JSON string representation. |
165
|
|
|
|
166
|
|
|
""" |
167
|
1 |
|
return json.dumps(self.as_dict(include_id), sort_keys=sort_keys) |
168
|
|
|
|
169
|
1 |
|
def as_of_add_flow_mod(self): |
170
|
|
|
"""Return an OpenFlow add FlowMod.""" |
171
|
1 |
|
return self._as_of_flow_mod(FlowModCommand.OFPFC_ADD) |
172
|
|
|
|
173
|
1 |
|
def as_of_delete_flow_mod(self): |
174
|
|
|
"""Return an OpenFlow delete FlowMod.""" |
175
|
1 |
|
return self._as_of_flow_mod(FlowModCommand.OFPFC_DELETE) |
176
|
|
|
|
177
|
1 |
|
@abstractmethod |
178
|
|
|
def _as_of_flow_mod(self, command): |
179
|
|
|
"""Return a pyof FlowMod with given ``command``.""" |
180
|
|
|
# Disable not-callable error as subclasses will set a class |
181
|
1 |
|
flow_mod = self._flow_mod_class() # pylint: disable=E1102 |
182
|
1 |
|
flow_mod.match = self.match.as_of_match() |
183
|
1 |
|
flow_mod.cookie = self.cookie |
184
|
1 |
|
flow_mod.command = command |
185
|
1 |
|
flow_mod.idle_timeout = self.idle_timeout |
186
|
1 |
|
flow_mod.hard_timeout = self.hard_timeout |
187
|
1 |
|
flow_mod.priority = self.priority |
188
|
1 |
|
return flow_mod |
189
|
|
|
|
190
|
1 |
|
@staticmethod |
191
|
1 |
|
@abstractmethod |
192
|
|
|
def _get_of_actions(of_flow_stats): |
193
|
|
|
"""Return pyof actions from pyof FlowStats.""" |
194
|
|
|
|
195
|
1 |
|
@classmethod |
196
|
|
|
def from_of_flow_stats(cls, of_flow_stats, switch): |
197
|
|
|
"""Create a flow with latest stats based on pyof FlowStats.""" |
198
|
1 |
|
of_actions = cls._get_of_actions(of_flow_stats) |
199
|
1 |
|
actions = (cls._action_factory.from_of_action(of_action) |
|
|
|
|
200
|
|
|
for of_action in of_actions) |
201
|
1 |
|
non_none_actions = [action for action in actions if action] |
202
|
1 |
|
return cls(switch, |
203
|
|
|
table_id=of_flow_stats.table_id.value, |
204
|
|
|
match=cls._match_class.from_of_match(of_flow_stats.match), |
205
|
|
|
priority=of_flow_stats.priority.value, |
206
|
|
|
idle_timeout=of_flow_stats.idle_timeout.value, |
207
|
|
|
hard_timeout=of_flow_stats.hard_timeout.value, |
208
|
|
|
cookie=of_flow_stats.cookie.value, |
209
|
|
|
actions=non_none_actions, |
210
|
|
|
stats=FlowStats.from_of_flow_stats(of_flow_stats)) |
211
|
|
|
|
212
|
|
|
|
213
|
1 |
|
class ActionBase(ABC): |
214
|
|
|
"""Base class for a flow action.""" |
215
|
|
|
|
216
|
1 |
|
def as_dict(self): |
217
|
|
|
"""Return a dict that can be dumped as JSON.""" |
218
|
1 |
|
return vars(self) |
219
|
|
|
|
220
|
1 |
|
@classmethod |
221
|
|
|
def from_dict(cls, action_dict): |
222
|
|
|
"""Return an action instance from attributes in a dictionary.""" |
223
|
1 |
|
action = cls(None) |
224
|
1 |
|
for attr_name, value in action_dict.items(): |
225
|
1 |
|
if hasattr(action, attr_name): |
226
|
1 |
|
setattr(action, attr_name, value) |
227
|
1 |
|
return action |
228
|
|
|
|
229
|
1 |
|
@abstractmethod |
230
|
|
|
def as_of_action(self): |
231
|
|
|
"""Return a pyof action to be used by a FlowMod.""" |
232
|
|
|
|
233
|
1 |
|
@classmethod |
234
|
1 |
|
@abstractmethod |
235
|
|
|
def from_of_action(cls, of_action): |
236
|
|
|
"""Return an action from a pyof action.""" |
237
|
|
|
|
238
|
|
|
|
239
|
1 |
|
class ActionFactoryBase(ABC): |
240
|
|
|
"""Deal with different implementations of ActionBase.""" |
241
|
|
|
|
242
|
|
|
# key: action_type or pyof class, value: ActionBase child |
243
|
1 |
|
_action_class = { |
244
|
|
|
'output': None, |
245
|
|
|
'set_vlan': None, |
246
|
|
|
# pyof class: ActionBase child |
247
|
|
|
} |
248
|
|
|
|
249
|
1 |
|
@classmethod |
250
|
|
|
def from_dict(cls, action_dict): |
251
|
|
|
"""Build the proper Action from a dictionary. |
252
|
|
|
|
253
|
|
|
Args: |
254
|
|
|
action_dict (dict): Action attributes. |
255
|
|
|
""" |
256
|
1 |
|
action_type = action_dict.get('action_type') |
257
|
1 |
|
action_class = cls._action_class[action_type] |
258
|
1 |
|
return action_class.from_dict(action_dict) if action_class else None |
259
|
|
|
|
260
|
1 |
|
@classmethod |
261
|
|
|
def from_of_action(cls, of_action): |
262
|
|
|
"""Build the proper Action from a pyof action. |
263
|
|
|
|
264
|
|
|
Args: |
265
|
|
|
of_action (pyof action): Action from python-openflow. |
266
|
|
|
""" |
267
|
1 |
|
of_class = type(of_action) |
268
|
1 |
|
action_class = cls._action_class.get(of_class) |
269
|
1 |
|
return action_class.from_of_action(of_action) if action_class else None |
270
|
|
|
|
271
|
|
|
|
272
|
1 |
|
class MatchBase: # pylint: disable=too-many-instance-attributes |
273
|
|
|
"""Base class with common high-level Match fields.""" |
274
|
|
|
|
275
|
1 |
|
def __init__(self, in_port=None, dl_src=None, dl_dst=None, dl_vlan=None, |
276
|
|
|
dl_vlan_pcp=None, dl_type=None, nw_proto=None, nw_src=None, |
277
|
|
|
nw_dst=None, tp_src=None, tp_dst=None): |
278
|
|
|
"""Make it possible to set all attributes from the constructor.""" |
279
|
|
|
# pylint: disable=too-many-arguments |
280
|
1 |
|
self.in_port = in_port |
281
|
1 |
|
self.dl_src = dl_src |
282
|
1 |
|
self.dl_dst = dl_dst |
283
|
1 |
|
self.dl_vlan = dl_vlan |
284
|
1 |
|
self.dl_vlan_pcp = dl_vlan_pcp |
285
|
1 |
|
self.dl_type = dl_type |
286
|
1 |
|
self.nw_proto = nw_proto |
287
|
1 |
|
self.nw_src = nw_src |
288
|
1 |
|
self.nw_dst = nw_dst |
289
|
1 |
|
self.tp_src = tp_src |
290
|
1 |
|
self.tp_dst = tp_dst |
291
|
|
|
|
292
|
1 |
|
def as_dict(self): |
293
|
|
|
"""Return a dictionary excluding ``None`` values.""" |
294
|
1 |
|
return {k: v for k, v in self.__dict__.items() if v is not None} |
295
|
|
|
|
296
|
1 |
|
@classmethod |
297
|
|
|
def from_dict(cls, match_dict): |
298
|
|
|
"""Return a Match instance from a dictionary.""" |
299
|
1 |
|
match = cls() |
300
|
1 |
|
for key, value in match_dict.items(): |
301
|
1 |
|
if key in match.__dict__: |
302
|
1 |
|
setattr(match, key, value) |
303
|
1 |
|
return match |
304
|
|
|
|
305
|
1 |
|
@classmethod |
306
|
1 |
|
@abstractmethod |
307
|
|
|
def from_of_match(cls, of_match): |
308
|
|
|
"""Return a Match instance from a pyof Match.""" |
309
|
|
|
|
310
|
1 |
|
@abstractmethod |
311
|
|
|
def as_of_match(self): |
312
|
|
|
"""Return a python-openflow Match.""" |
313
|
|
|
|
314
|
|
|
|
315
|
1 |
|
class Stats: |
316
|
|
|
"""Simple class to store statistics as attributes and values.""" |
317
|
|
|
|
318
|
1 |
|
def as_dict(self): |
319
|
|
|
"""Return a dict excluding attributes with ``None`` value.""" |
320
|
1 |
|
return {attribute: value |
321
|
|
|
for attribute, value in vars(self).items() |
322
|
|
|
if value is not None} |
323
|
|
|
|
324
|
1 |
|
@classmethod |
325
|
|
|
def from_dict(cls, stats_dict): |
326
|
|
|
"""Return a statistics object from a dictionary.""" |
327
|
1 |
|
stats = cls() |
328
|
1 |
|
cls._update(stats, stats_dict.items()) |
329
|
1 |
|
return stats |
330
|
|
|
|
331
|
1 |
|
@classmethod |
332
|
|
|
def from_of_flow_stats(cls, of_stats): |
333
|
|
|
"""Create an instance from a pyof FlowStats.""" |
334
|
1 |
|
stats = cls() |
335
|
1 |
|
stats.update(of_stats) |
336
|
1 |
|
return stats |
337
|
|
|
|
338
|
1 |
|
def update(self, of_stats): |
339
|
|
|
"""Given a pyof stats object, update attributes' values. |
340
|
|
|
|
341
|
|
|
Avoid object creation and memory leak. pyof values are GenericType |
342
|
|
|
instances whose native values can be accessed by `.value`. |
343
|
|
|
""" |
344
|
|
|
# Generator for GenericType values |
345
|
1 |
|
attr_name_value = ((attr_name, gen_type.value) |
|
|
|
|
346
|
|
|
for attr_name, gen_type in vars(of_stats).items() |
347
|
|
|
if attr_name in vars(self)) |
348
|
1 |
|
self._update(self, attr_name_value) |
349
|
|
|
|
350
|
1 |
|
@staticmethod |
351
|
|
|
def _update(obj, iterable): |
352
|
|
|
"""From attribute name and value pairs, update ``obj``.""" |
353
|
1 |
|
for attr_name, value in iterable: |
354
|
1 |
|
if hasattr(obj, attr_name): |
355
|
1 |
|
setattr(obj, attr_name, value) |
356
|
|
|
|
357
|
|
|
|
358
|
1 |
|
class FlowStats(Stats): |
359
|
|
|
"""Common fields for 1.0 and 1.3 FlowStats.""" |
360
|
|
|
|
361
|
1 |
|
def __init__(self): |
362
|
|
|
"""Initialize all statistics as ``None``.""" |
363
|
1 |
|
self.byte_count = None |
364
|
1 |
|
self.duration_sec = None |
365
|
1 |
|
self.duration_nsec = None |
366
|
1 |
|
self.packet_count = None |
367
|
|
|
|
368
|
|
|
|
369
|
1 |
|
class PortStats(Stats): # pylint: disable=too-many-instance-attributes |
370
|
|
|
"""Common fields for 1.0 and 1.3 PortStats.""" |
371
|
|
|
|
372
|
1 |
|
def __init__(self): |
373
|
|
|
"""Initialize all statistics as ``None``.""" |
374
|
|
|
self.rx_packets = None |
375
|
|
|
self.tx_packets = None |
376
|
|
|
self.rx_bytes = None |
377
|
|
|
self.tx_bytes = None |
378
|
|
|
self.rx_dropped = None |
379
|
|
|
self.tx_dropped = None |
380
|
|
|
self.rx_errors = None |
381
|
|
|
self.tx_errors = None |
382
|
|
|
self.rx_frame_err = None |
383
|
|
|
self.rx_over_err = None |
384
|
|
|
self.rx_crc_err = None |
385
|
|
|
self.collisions = None |
386
|
|
|
|