|
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=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
|
|
|
of_actions = cls._get_of_actions(of_flow_stats) |
|
199
|
|
|
actions = (cls._action_factory.from_of_action(of_action) |
|
|
|
|
|
|
200
|
|
|
for of_action in of_actions) |
|
201
|
|
|
non_none_actions = [action for action in actions if action] |
|
202
|
|
|
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
|
|
|
of_class = type(of_action) |
|
268
|
|
|
action_class = cls._action_class.get(of_class) |
|
269
|
|
|
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, in_phy_port=None, |
|
278
|
|
|
ip_dscp=None, ip_ecn=None, udp_src=None, udp_dst=None, |
|
279
|
|
|
sctp_src=None, sctp_dst=None, icmpv4_type=None, |
|
280
|
|
|
icmpv4_code=None, arp_op=None, arp_spa=None, arp_tpa=None, |
|
281
|
|
|
arp_sha=None, arp_tha=None, ipv6_src=None, ipv6_dst=None, |
|
282
|
|
|
ipv6_flabel=None, icmpv6_type=None, icmpv6_code=None, |
|
283
|
|
|
nd_tar=None, nd_sll=None, nd_tll=None, mpls_lab=None, |
|
284
|
|
|
mpls_tc=None, mpls_bos=None, pbb_isid=None, v6_hdr=None, |
|
285
|
|
|
metadata=None, tun_id=None): |
|
286
|
|
|
"""Make it possible to set all attributes from the constructor.""" |
|
287
|
|
|
# pylint: disable=too-many-arguments |
|
288
|
|
|
# pylint: disable=too-many-locals |
|
289
|
1 |
|
self.in_port = in_port |
|
290
|
1 |
|
self.dl_src = dl_src |
|
291
|
1 |
|
self.dl_dst = dl_dst |
|
292
|
1 |
|
self.dl_vlan = dl_vlan |
|
293
|
1 |
|
self.dl_vlan_pcp = dl_vlan_pcp |
|
294
|
1 |
|
self.dl_type = dl_type |
|
295
|
1 |
|
self.nw_proto = nw_proto |
|
296
|
1 |
|
self.nw_src = nw_src |
|
297
|
1 |
|
self.nw_dst = nw_dst |
|
298
|
1 |
|
self.tp_src = tp_src |
|
299
|
1 |
|
self.tp_dst = tp_dst |
|
300
|
1 |
|
self.in_phy_port = in_phy_port |
|
301
|
1 |
|
self.ip_dscp = ip_dscp |
|
302
|
1 |
|
self.ip_ecn = ip_ecn |
|
303
|
1 |
|
self.udp_src = udp_src |
|
304
|
1 |
|
self.udp_dst = udp_dst |
|
305
|
1 |
|
self.sctp_src = sctp_src |
|
306
|
1 |
|
self.sctp_dst = sctp_dst |
|
307
|
1 |
|
self.icmpv4_type = icmpv4_type |
|
308
|
1 |
|
self.icmpv4_code = icmpv4_code |
|
309
|
1 |
|
self.arp_op = arp_op |
|
310
|
1 |
|
self.arp_spa = arp_spa |
|
311
|
1 |
|
self.arp_tpa = arp_tpa |
|
312
|
1 |
|
self.arp_sha = arp_sha |
|
313
|
1 |
|
self.arp_tha = arp_tha |
|
314
|
1 |
|
self.ipv6_src = ipv6_src |
|
315
|
1 |
|
self.ipv6_dst = ipv6_dst |
|
316
|
1 |
|
self.ipv6_flabel = ipv6_flabel |
|
317
|
1 |
|
self.icmpv6_type = icmpv6_type |
|
318
|
1 |
|
self.icmpv6_code = icmpv6_code |
|
319
|
1 |
|
self.nd_tar = nd_tar |
|
320
|
1 |
|
self.nd_sll = nd_sll |
|
321
|
1 |
|
self.nd_tll = nd_tll |
|
322
|
1 |
|
self.mpls_lab = mpls_lab |
|
323
|
1 |
|
self.mpls_tc = mpls_tc |
|
324
|
1 |
|
self.mpls_bos = mpls_bos |
|
325
|
1 |
|
self.pbb_isid = pbb_isid |
|
326
|
1 |
|
self.v6_hdr = v6_hdr |
|
327
|
1 |
|
self.metadata = metadata |
|
328
|
1 |
|
self.tun_id = tun_id |
|
329
|
|
|
|
|
330
|
1 |
|
def as_dict(self): |
|
331
|
|
|
"""Return a dictionary excluding ``None`` values.""" |
|
332
|
1 |
|
return {k: v for k, v in self.__dict__.items() if v is not None} |
|
333
|
|
|
|
|
334
|
1 |
|
@classmethod |
|
335
|
|
|
def from_dict(cls, match_dict): |
|
336
|
|
|
"""Return a Match instance from a dictionary.""" |
|
337
|
1 |
|
match = cls() |
|
338
|
1 |
|
for key, value in match_dict.items(): |
|
339
|
1 |
|
if key in match.__dict__: |
|
340
|
1 |
|
setattr(match, key, value) |
|
341
|
1 |
|
return match |
|
342
|
|
|
|
|
343
|
1 |
|
@classmethod |
|
344
|
1 |
|
@abstractmethod |
|
345
|
|
|
def from_of_match(cls, of_match): |
|
346
|
|
|
"""Return a Match instance from a pyof Match.""" |
|
347
|
|
|
|
|
348
|
1 |
|
@abstractmethod |
|
349
|
|
|
def as_of_match(self): |
|
350
|
|
|
"""Return a python-openflow Match.""" |
|
351
|
|
|
|
|
352
|
|
|
|
|
353
|
1 |
|
class Stats: |
|
354
|
|
|
"""Simple class to store statistics as attributes and values.""" |
|
355
|
|
|
|
|
356
|
1 |
|
def as_dict(self): |
|
357
|
|
|
"""Return a dict excluding attributes with ``None`` value.""" |
|
358
|
1 |
|
return {attribute: value |
|
359
|
|
|
for attribute, value in vars(self).items() |
|
360
|
|
|
if value is not None} |
|
361
|
|
|
|
|
362
|
1 |
|
@classmethod |
|
363
|
|
|
def from_dict(cls, stats_dict): |
|
364
|
|
|
"""Return a statistics object from a dictionary.""" |
|
365
|
1 |
|
stats = cls() |
|
366
|
1 |
|
cls._update(stats, stats_dict.items()) |
|
367
|
1 |
|
return stats |
|
368
|
|
|
|
|
369
|
1 |
|
@classmethod |
|
370
|
|
|
def from_of_flow_stats(cls, of_stats): |
|
371
|
|
|
"""Create an instance from a pyof FlowStats.""" |
|
372
|
|
|
stats = cls() |
|
373
|
|
|
stats.update(of_stats) |
|
374
|
|
|
return stats |
|
375
|
|
|
|
|
376
|
1 |
|
def update(self, of_stats): |
|
377
|
|
|
"""Given a pyof stats object, update attributes' values. |
|
378
|
|
|
|
|
379
|
|
|
Avoid object creation and memory leak. pyof values are GenericType |
|
380
|
|
|
instances whose native values can be accessed by `.value`. |
|
381
|
|
|
""" |
|
382
|
|
|
# Generator for GenericType values |
|
383
|
|
|
attr_name_value = ((attr_name, gen_type.value) |
|
|
|
|
|
|
384
|
|
|
for attr_name, gen_type in vars(of_stats).items() |
|
385
|
|
|
if attr_name in vars(self)) |
|
386
|
|
|
self._update(self, attr_name_value) |
|
387
|
|
|
|
|
388
|
1 |
|
@staticmethod |
|
389
|
|
|
def _update(obj, iterable): |
|
390
|
|
|
"""From attribute name and value pairs, update ``obj``.""" |
|
391
|
1 |
|
for attr_name, value in iterable: |
|
392
|
|
|
if hasattr(obj, attr_name): |
|
393
|
|
|
setattr(obj, attr_name, value) |
|
394
|
|
|
|
|
395
|
|
|
|
|
396
|
1 |
|
class FlowStats(Stats): |
|
397
|
|
|
"""Common fields for 1.0 and 1.3 FlowStats.""" |
|
398
|
|
|
|
|
399
|
1 |
|
def __init__(self): |
|
400
|
|
|
"""Initialize all statistics as ``None``.""" |
|
401
|
1 |
|
self.byte_count = None |
|
402
|
1 |
|
self.duration_sec = None |
|
403
|
1 |
|
self.duration_nsec = None |
|
404
|
1 |
|
self.packet_count = None |
|
405
|
|
|
|
|
406
|
|
|
|
|
407
|
1 |
|
class PortStats(Stats): # pylint: disable=too-many-instance-attributes |
|
408
|
|
|
"""Common fields for 1.0 and 1.3 PortStats.""" |
|
409
|
|
|
|
|
410
|
1 |
|
def __init__(self): |
|
411
|
|
|
"""Initialize all statistics as ``None``.""" |
|
412
|
|
|
self.rx_packets = None |
|
413
|
|
|
self.tx_packets = None |
|
414
|
|
|
self.rx_bytes = None |
|
415
|
|
|
self.tx_bytes = None |
|
416
|
|
|
self.rx_dropped = None |
|
417
|
|
|
self.tx_dropped = None |
|
418
|
|
|
self.rx_errors = None |
|
419
|
|
|
self.tx_errors = None |
|
420
|
|
|
self.rx_frame_err = None |
|
421
|
|
|
self.rx_over_err = None |
|
422
|
|
|
self.rx_crc_err = None |
|
423
|
|
|
self.collisions = None |
|
424
|
|
|
|