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
|
1 |
|
flow_class = cls.get_class(switch) |
24
|
1 |
|
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
|
1 |
|
of_version = switch.connection.protocol.version |
30
|
1 |
|
if of_version == 0x01: |
31
|
1 |
|
return v0x01.flow.Flow |
32
|
1 |
|
if of_version == 0x04: |
33
|
1 |
|
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=0x0, 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
|
|
|
(0xff is valid only for the delete command) |
63
|
|
|
match (|match|): Match object. |
64
|
|
|
priority (int): Priority level of flow entry. |
65
|
|
|
idle_timeout (int): Idle time before discarding, in seconds. |
66
|
|
|
hard_timeout (int): Max time before discarding, in seconds. |
67
|
|
|
cookie (int): Opaque controller-issued identifier. |
68
|
|
|
actions (|list_of_actions|): List of actions to apply. |
69
|
|
|
stats (Stats): Latest flow statistics. |
70
|
|
|
""" |
71
|
|
|
# pylint: disable=too-many-arguments,too-many-locals |
72
|
1 |
|
self.switch = switch |
73
|
1 |
|
self.table_id = table_id |
74
|
|
|
# Disable not-callable error as subclasses set a class |
75
|
1 |
|
self.match = match or self._match_class() # pylint: disable=E1102 |
76
|
1 |
|
self.priority = priority |
77
|
1 |
|
self.idle_timeout = idle_timeout |
78
|
1 |
|
self.hard_timeout = hard_timeout |
79
|
1 |
|
self.cookie = cookie |
80
|
1 |
|
self.actions = actions or [] |
81
|
1 |
|
self.stats = stats or FlowStats() # pylint: disable=E1102 |
82
|
|
|
|
83
|
1 |
|
@property |
84
|
|
|
def id(self): # pylint: disable=invalid-name |
85
|
|
|
"""Return this flow unique identifier. |
86
|
|
|
|
87
|
|
|
Calculate an md5 hash based on this object's modified json string. The |
88
|
|
|
json for ID calculation excludes ``stats`` attribute that changes over |
89
|
|
|
time. |
90
|
|
|
|
91
|
|
|
Returns: |
92
|
|
|
str: Flow unique identifier (md5sum). |
93
|
|
|
|
94
|
|
|
""" |
95
|
1 |
|
flow_str = self.as_json(sort_keys=True, include_id=False) |
96
|
1 |
|
md5sum = md5() |
97
|
1 |
|
md5sum.update(flow_str.encode('utf-8')) |
98
|
1 |
|
return md5sum.hexdigest() |
99
|
|
|
|
100
|
1 |
|
def as_dict(self, include_id=True): |
101
|
|
|
"""Return the Flow as a serializable Python dictionary. |
102
|
|
|
|
103
|
|
|
Args: |
104
|
|
|
include_id (bool): Default is ``True``. Internally, it is set to |
105
|
|
|
``False`` when calculating the flow ID that is based in this |
106
|
|
|
dictionary's JSON string. |
107
|
|
|
|
108
|
|
|
Returns: |
109
|
|
|
dict: Serializable dictionary. |
110
|
|
|
|
111
|
|
|
""" |
112
|
1 |
|
flow_dict = { |
113
|
|
|
'switch': self.switch.id, |
114
|
|
|
'table_id': self.table_id, |
115
|
|
|
'match': self.match.as_dict(), |
116
|
|
|
'priority': self.priority, |
117
|
|
|
'idle_timeout': self.idle_timeout, |
118
|
|
|
'hard_timeout': self.hard_timeout, |
119
|
|
|
'cookie': self.cookie, |
120
|
|
|
'actions': [action.as_dict() for action in self.actions]} |
121
|
1 |
|
if include_id: |
122
|
|
|
# Avoid infinite recursion |
123
|
1 |
|
flow_dict['id'] = self.id |
124
|
|
|
# Remove statistics that change over time |
125
|
1 |
|
flow_dict['stats'] = self.stats.as_dict() |
126
|
|
|
|
127
|
1 |
|
return flow_dict |
128
|
|
|
|
129
|
1 |
|
@classmethod |
130
|
|
|
def from_dict(cls, flow_dict, switch): |
131
|
|
|
"""Return an instance with values from ``flow_dict``.""" |
132
|
1 |
|
flow = cls(switch) |
133
|
|
|
|
134
|
|
|
# Set attributes found in ``flow_dict`` |
135
|
1 |
|
for attr_name, attr_value in flow_dict.items(): |
136
|
1 |
|
if attr_name in vars(flow): |
137
|
1 |
|
setattr(flow, attr_name, attr_value) |
138
|
|
|
|
139
|
1 |
|
flow.switch = switch |
140
|
1 |
|
if 'stats' in flow_dict: |
141
|
1 |
|
flow.stats = FlowStats.from_dict(flow_dict['stats']) |
142
|
|
|
|
143
|
|
|
# Version-specific attributes |
144
|
1 |
|
if 'match' in flow_dict: |
145
|
1 |
|
flow.match = cls._match_class.from_dict(flow_dict['match']) |
146
|
1 |
|
if 'actions' in flow_dict: |
147
|
1 |
|
flow.actions = [] |
148
|
1 |
|
for action_dict in flow_dict['actions']: |
149
|
1 |
|
action = cls._action_factory.from_dict(action_dict) |
150
|
1 |
|
if action: |
151
|
1 |
|
flow.actions.append(action) |
152
|
|
|
|
153
|
1 |
|
return flow |
154
|
|
|
|
155
|
1 |
|
def as_json(self, sort_keys=False, include_id=True): |
156
|
|
|
"""Return the representation of a flow in JSON format. |
157
|
|
|
|
158
|
|
|
Args: |
159
|
|
|
sort_keys (bool): ``False`` by default (Python's default). Sorting |
160
|
|
|
is used, for example, to calculate the flow ID. |
161
|
|
|
include_id (bool): ``True`` by default. Internally, the ID is not |
162
|
|
|
included while calculating it. |
163
|
|
|
|
164
|
|
|
Returns: |
165
|
|
|
string: Flow JSON string representation. |
166
|
|
|
|
167
|
|
|
""" |
168
|
1 |
|
version = self.switch.connection.protocol.version |
169
|
1 |
|
if version == 0x01: |
170
|
1 |
|
return json.dumps(self.as_dict(include_id), |
171
|
|
|
cls=v0x01.utils.JSONEncoderOF10, |
172
|
|
|
sort_keys=sort_keys) |
173
|
1 |
|
return json.dumps(self.as_dict(include_id), sort_keys=sort_keys) |
174
|
|
|
|
175
|
1 |
|
def as_of_add_flow_mod(self): |
176
|
|
|
"""Return an OpenFlow add FlowMod.""" |
177
|
1 |
|
return self._as_of_flow_mod(FlowModCommand.OFPFC_ADD) |
178
|
|
|
|
179
|
1 |
|
def as_of_delete_flow_mod(self): |
180
|
|
|
"""Return an OpenFlow delete FlowMod.""" |
181
|
1 |
|
return self._as_of_flow_mod(FlowModCommand.OFPFC_DELETE) |
182
|
|
|
|
183
|
1 |
|
def as_of_strict_delete_flow_mod(self): |
184
|
|
|
"""Return an OpenFlow strict delete FlowMod.""" |
185
|
|
|
return self._as_of_flow_mod(FlowModCommand.OFPFC_DELETE_STRICT) |
186
|
|
|
|
187
|
1 |
|
@abstractmethod |
188
|
|
|
def _as_of_flow_mod(self, command): |
189
|
|
|
"""Return a pyof FlowMod with given ``command``.""" |
190
|
|
|
# Disable not-callable error as subclasses will set a class |
191
|
1 |
|
flow_mod = self._flow_mod_class() # pylint: disable=E1102 |
192
|
1 |
|
flow_mod.match = self.match.as_of_match() |
193
|
1 |
|
flow_mod.cookie = self.cookie |
194
|
1 |
|
flow_mod.command = command |
195
|
1 |
|
flow_mod.idle_timeout = self.idle_timeout |
196
|
1 |
|
flow_mod.hard_timeout = self.hard_timeout |
197
|
1 |
|
flow_mod.priority = self.priority |
198
|
1 |
|
flow_mod.table_id = self.table_id |
199
|
1 |
|
return flow_mod |
200
|
|
|
|
201
|
1 |
|
@staticmethod |
202
|
1 |
|
@abstractmethod |
203
|
|
|
def _get_of_actions(of_flow_stats): |
204
|
|
|
"""Return pyof actions from pyof FlowStats.""" |
205
|
|
|
|
206
|
1 |
|
@classmethod |
207
|
|
|
def from_of_flow_stats(cls, of_flow_stats, switch): |
208
|
|
|
"""Create a flow with latest stats based on pyof FlowStats.""" |
209
|
|
|
of_actions = cls._get_of_actions(of_flow_stats) |
210
|
|
|
actions = (cls._action_factory.from_of_action(of_action) |
|
|
|
|
211
|
|
|
for of_action in of_actions) |
212
|
|
|
non_none_actions = [action for action in actions if action] |
213
|
|
|
return cls(switch, |
214
|
|
|
table_id=of_flow_stats.table_id.value, |
215
|
|
|
match=cls._match_class.from_of_match(of_flow_stats.match), |
216
|
|
|
priority=of_flow_stats.priority.value, |
217
|
|
|
idle_timeout=of_flow_stats.idle_timeout.value, |
218
|
|
|
hard_timeout=of_flow_stats.hard_timeout.value, |
219
|
|
|
cookie=of_flow_stats.cookie.value, |
220
|
|
|
actions=non_none_actions, |
221
|
|
|
stats=FlowStats.from_of_flow_stats(of_flow_stats)) |
222
|
|
|
|
223
|
1 |
|
def __eq__(self, other): |
224
|
1 |
|
include_id = False |
225
|
1 |
|
if not isinstance(other, self.__class__): |
226
|
1 |
|
raise ValueError(f'Error comparing flows: {other} is not ' |
227
|
|
|
f'an instance of {self.__class__}') |
228
|
|
|
|
229
|
1 |
|
return self.as_dict(include_id) == other.as_dict(include_id) |
230
|
|
|
|
231
|
|
|
|
232
|
1 |
|
class ActionBase(ABC): |
233
|
|
|
"""Base class for a flow action.""" |
234
|
|
|
|
235
|
1 |
|
def as_dict(self): |
236
|
|
|
"""Return a dict that can be dumped as JSON.""" |
237
|
1 |
|
return vars(self) |
238
|
|
|
|
239
|
1 |
|
@classmethod |
240
|
|
|
def from_dict(cls, action_dict): |
241
|
|
|
"""Return an action instance from attributes in a dictionary.""" |
242
|
1 |
|
action = cls(None) |
243
|
1 |
|
for attr_name, value in action_dict.items(): |
244
|
1 |
|
if hasattr(action, attr_name): |
245
|
1 |
|
setattr(action, attr_name, value) |
246
|
1 |
|
return action |
247
|
|
|
|
248
|
1 |
|
@abstractmethod |
249
|
|
|
def as_of_action(self): |
250
|
|
|
"""Return a pyof action to be used by a FlowMod.""" |
251
|
|
|
|
252
|
1 |
|
@classmethod |
253
|
1 |
|
@abstractmethod |
254
|
|
|
def from_of_action(cls, of_action): |
255
|
|
|
"""Return an action from a pyof action.""" |
256
|
|
|
|
257
|
|
|
|
258
|
1 |
|
class ActionFactoryBase(ABC): |
259
|
|
|
"""Deal with different implementations of ActionBase.""" |
260
|
|
|
|
261
|
|
|
# key: action_type or pyof class, value: ActionBase child |
262
|
1 |
|
_action_class = { |
263
|
|
|
'output': None, |
264
|
|
|
'set_vlan': None, |
265
|
|
|
# pyof class: ActionBase child |
266
|
|
|
} |
267
|
|
|
|
268
|
1 |
|
@classmethod |
269
|
|
|
def from_dict(cls, action_dict): |
270
|
|
|
"""Build the proper Action from a dictionary. |
271
|
|
|
|
272
|
|
|
Args: |
273
|
|
|
action_dict (dict): Action attributes. |
274
|
|
|
""" |
275
|
1 |
|
action_type = action_dict.get('action_type') |
276
|
1 |
|
action_class = cls._action_class[action_type] |
277
|
1 |
|
return action_class.from_dict(action_dict) if action_class else None |
278
|
|
|
|
279
|
1 |
|
@classmethod |
280
|
|
|
def from_of_action(cls, of_action): |
281
|
|
|
"""Build the proper Action from a pyof action. |
282
|
|
|
|
283
|
|
|
Args: |
284
|
|
|
of_action (pyof action): Action from python-openflow. |
285
|
|
|
""" |
286
|
|
|
of_class = type(of_action) |
287
|
|
|
action_class = cls._action_class.get(of_class) |
288
|
|
|
return action_class.from_of_action(of_action) if action_class else None |
289
|
|
|
|
290
|
|
|
|
291
|
1 |
|
class MatchBase: # pylint: disable=too-many-instance-attributes |
292
|
|
|
"""Base class with common high-level Match fields.""" |
293
|
|
|
|
294
|
1 |
|
def __init__(self, in_port=None, dl_src=None, dl_dst=None, dl_vlan=None, |
295
|
|
|
dl_vlan_pcp=None, dl_type=None, nw_proto=None, nw_src=None, |
296
|
|
|
nw_dst=None, tp_src=None, tp_dst=None, in_phy_port=None, |
297
|
|
|
ip_dscp=None, ip_ecn=None, udp_src=None, udp_dst=None, |
298
|
|
|
sctp_src=None, sctp_dst=None, icmpv4_type=None, |
299
|
|
|
icmpv4_code=None, arp_op=None, arp_spa=None, arp_tpa=None, |
300
|
|
|
arp_sha=None, arp_tha=None, ipv6_src=None, ipv6_dst=None, |
301
|
|
|
ipv6_flabel=None, icmpv6_type=None, icmpv6_code=None, |
302
|
|
|
nd_tar=None, nd_sll=None, nd_tll=None, mpls_lab=None, |
303
|
|
|
mpls_tc=None, mpls_bos=None, pbb_isid=None, v6_hdr=None, |
304
|
|
|
metadata=None, tun_id=None): |
305
|
|
|
"""Make it possible to set all attributes from the constructor.""" |
306
|
|
|
# pylint: disable=too-many-arguments |
307
|
|
|
# pylint: disable=too-many-locals |
308
|
1 |
|
self.in_port = in_port |
309
|
1 |
|
self.dl_src = dl_src |
310
|
1 |
|
self.dl_dst = dl_dst |
311
|
1 |
|
self.dl_vlan = dl_vlan |
312
|
1 |
|
self.dl_vlan_pcp = dl_vlan_pcp |
313
|
1 |
|
self.dl_type = dl_type |
314
|
1 |
|
self.nw_proto = nw_proto |
315
|
1 |
|
self.nw_src = nw_src |
316
|
1 |
|
self.nw_dst = nw_dst |
317
|
1 |
|
self.tp_src = tp_src |
318
|
1 |
|
self.tp_dst = tp_dst |
319
|
1 |
|
self.in_phy_port = in_phy_port |
320
|
1 |
|
self.ip_dscp = ip_dscp |
321
|
1 |
|
self.ip_ecn = ip_ecn |
322
|
1 |
|
self.udp_src = udp_src |
323
|
1 |
|
self.udp_dst = udp_dst |
324
|
1 |
|
self.sctp_src = sctp_src |
325
|
1 |
|
self.sctp_dst = sctp_dst |
326
|
1 |
|
self.icmpv4_type = icmpv4_type |
327
|
1 |
|
self.icmpv4_code = icmpv4_code |
328
|
1 |
|
self.arp_op = arp_op |
329
|
1 |
|
self.arp_spa = arp_spa |
330
|
1 |
|
self.arp_tpa = arp_tpa |
331
|
1 |
|
self.arp_sha = arp_sha |
332
|
1 |
|
self.arp_tha = arp_tha |
333
|
1 |
|
self.ipv6_src = ipv6_src |
334
|
1 |
|
self.ipv6_dst = ipv6_dst |
335
|
1 |
|
self.ipv6_flabel = ipv6_flabel |
336
|
1 |
|
self.icmpv6_type = icmpv6_type |
337
|
1 |
|
self.icmpv6_code = icmpv6_code |
338
|
1 |
|
self.nd_tar = nd_tar |
339
|
1 |
|
self.nd_sll = nd_sll |
340
|
1 |
|
self.nd_tll = nd_tll |
341
|
1 |
|
self.mpls_lab = mpls_lab |
342
|
1 |
|
self.mpls_tc = mpls_tc |
343
|
1 |
|
self.mpls_bos = mpls_bos |
344
|
1 |
|
self.pbb_isid = pbb_isid |
345
|
1 |
|
self.v6_hdr = v6_hdr |
346
|
1 |
|
self.metadata = metadata |
347
|
1 |
|
self.tun_id = tun_id |
348
|
|
|
|
349
|
1 |
|
def as_dict(self): |
350
|
|
|
"""Return a dictionary excluding ``None`` values.""" |
351
|
1 |
|
return {k: v for k, v in self.__dict__.items() if v is not None} |
352
|
|
|
|
353
|
1 |
|
@classmethod |
354
|
|
|
def from_dict(cls, match_dict): |
355
|
|
|
"""Return a Match instance from a dictionary.""" |
356
|
1 |
|
match = cls() |
357
|
1 |
|
for key, value in match_dict.items(): |
358
|
1 |
|
if key in match.__dict__: |
359
|
1 |
|
setattr(match, key, value) |
360
|
1 |
|
return match |
361
|
|
|
|
362
|
1 |
|
@classmethod |
363
|
1 |
|
@abstractmethod |
364
|
|
|
def from_of_match(cls, of_match): |
365
|
|
|
"""Return a Match instance from a pyof Match.""" |
366
|
|
|
|
367
|
1 |
|
@abstractmethod |
368
|
|
|
def as_of_match(self): |
369
|
|
|
"""Return a python-openflow Match.""" |
370
|
|
|
|
371
|
|
|
|
372
|
1 |
|
class Stats: |
373
|
|
|
"""Simple class to store statistics as attributes and values.""" |
374
|
|
|
|
375
|
1 |
|
def as_dict(self): |
376
|
|
|
"""Return a dict excluding attributes with ``None`` value.""" |
377
|
1 |
|
return {attribute: value |
378
|
|
|
for attribute, value in vars(self).items() |
379
|
|
|
if value is not None} |
380
|
|
|
|
381
|
1 |
|
@classmethod |
382
|
|
|
def from_dict(cls, stats_dict): |
383
|
|
|
"""Return a statistics object from a dictionary.""" |
384
|
1 |
|
stats = cls() |
385
|
1 |
|
cls._update(stats, stats_dict.items()) |
386
|
1 |
|
return stats |
387
|
|
|
|
388
|
1 |
|
@classmethod |
389
|
|
|
def from_of_flow_stats(cls, of_stats): |
390
|
|
|
"""Create an instance from a pyof FlowStats.""" |
391
|
|
|
stats = cls() |
392
|
|
|
stats.update(of_stats) |
393
|
|
|
return stats |
394
|
|
|
|
395
|
1 |
|
def update(self, of_stats): |
396
|
|
|
"""Given a pyof stats object, update attributes' values. |
397
|
|
|
|
398
|
|
|
Avoid object creation and memory leak. pyof values are GenericType |
399
|
|
|
instances whose native values can be accessed by `.value`. |
400
|
|
|
""" |
401
|
|
|
# Generator for GenericType values |
402
|
|
|
attr_name_value = ((attr_name, gen_type.value) |
|
|
|
|
403
|
|
|
for attr_name, gen_type in vars(of_stats).items() |
404
|
|
|
if attr_name in vars(self)) |
405
|
|
|
self._update(self, attr_name_value) |
406
|
|
|
|
407
|
1 |
|
@staticmethod |
408
|
|
|
def _update(obj, iterable): |
409
|
|
|
"""From attribute name and value pairs, update ``obj``.""" |
410
|
1 |
|
for attr_name, value in iterable: |
411
|
|
|
if hasattr(obj, attr_name): |
412
|
|
|
setattr(obj, attr_name, value) |
413
|
|
|
|
414
|
|
|
|
415
|
1 |
|
class FlowStats(Stats): |
416
|
|
|
"""Common fields for 1.0 and 1.3 FlowStats.""" |
417
|
|
|
|
418
|
1 |
|
def __init__(self): |
419
|
|
|
"""Initialize all statistics as ``None``.""" |
420
|
1 |
|
self.byte_count = None |
421
|
1 |
|
self.duration_sec = None |
422
|
1 |
|
self.duration_nsec = None |
423
|
1 |
|
self.packet_count = None |
424
|
|
|
|
425
|
|
|
|
426
|
1 |
|
class PortStats(Stats): # pylint: disable=too-many-instance-attributes |
427
|
|
|
"""Common fields for 1.0 and 1.3 PortStats.""" |
428
|
|
|
|
429
|
1 |
|
def __init__(self): |
430
|
|
|
"""Initialize all statistics as ``None``.""" |
431
|
|
|
self.rx_packets = None |
432
|
|
|
self.tx_packets = None |
433
|
|
|
self.rx_bytes = None |
434
|
|
|
self.tx_bytes = None |
435
|
|
|
self.rx_dropped = None |
436
|
|
|
self.tx_dropped = None |
437
|
|
|
self.rx_errors = None |
438
|
|
|
self.tx_errors = None |
439
|
|
|
self.rx_frame_err = None |
440
|
|
|
self.rx_over_err = None |
441
|
|
|
self.rx_crc_err = None |
442
|
|
|
self.collisions = None |
443
|
|
|
|