Passed
Push — master ( 667973...d237fc )
by Beraldo
01:46
created

build.flow.MatchBase.__init__()   A

Complexity

Conditions 1

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 14
nop 12
dl 0
loc 16
ccs 12
cts 12
cp 1
crap 1
rs 9.7
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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=0,
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
        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
        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
        flow_mod = self._flow_mod_class()  # pylint: disable=E1102
182
        flow_mod.match = self.match.as_of_match()
183
        flow_mod.cookie = self.cookie
184
        flow_mod.command = command
185
        flow_mod.idle_timeout = self.idle_timeout
186
        flow_mod.hard_timeout = self.hard_timeout
187
        flow_mod.priority = self.priority
188
        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
        pass
195
196 1
    @classmethod
197
    def from_of_flow_stats(cls, of_flow_stats, switch):
198
        """Create a flow with latest stats based on pyof FlowStats."""
199
        of_actions = cls._get_of_actions(of_flow_stats)
200
        actions = (cls._action_factory.from_of_action(of_action)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable of_action does not seem to be defined.
Loading history...
201
                   for of_action in of_actions)
202
        non_none_actions = [action for action in actions if action]
203
        return cls(switch,
204
                   table_id=of_flow_stats.table_id.value,
205
                   match=cls._match_class.from_of_match(of_flow_stats.match),
206
                   priority=of_flow_stats.priority.value,
207
                   idle_timeout=of_flow_stats.idle_timeout.value,
208
                   hard_timeout=of_flow_stats.hard_timeout.value,
209
                   cookie=of_flow_stats.cookie.value,
210
                   actions=non_none_actions,
211
                   stats=FlowStats.from_of_flow_stats(of_flow_stats))
212
213
214 1
class ActionBase(ABC):
215
    """Base class for a flow action."""
216
217 1
    def as_dict(self):
218
        """Return a dict that can be dumped as JSON."""
219 1
        return vars(self)
220
221 1
    @classmethod
222
    def from_dict(cls, action_dict):
223
        """Return an action instance from attributes in a dictionary."""
224 1
        action = cls(None)
225 1
        for attr_name, value in action_dict.items():
226 1
            if hasattr(action, attr_name):
227 1
                setattr(action, attr_name, value)
228 1
        return action
229
230 1
    @abstractmethod
231
    def as_of_action(self):
232
        """Return a pyof action to be used by a FlowMod."""
233
        pass
234
235 1
    @classmethod
236 1
    @abstractmethod
237
    def from_of_action(cls, of_action):
238
        """Return an action from a pyof action."""
239
        pass
240
241
242 1
class ActionFactoryBase(ABC):
243
    """Deal with different implementations of ActionBase."""
244
245
    # key: action_type or pyof class, value: ActionBase child
246 1
    _action_class = {
247
        'output': None,
248
        'set_vlan': None,
249
        # pyof class: ActionBase child
250
    }
251
252 1
    @classmethod
253
    def from_dict(cls, action_dict):
254
        """Build the proper Action from a dictionary.
255
256
        Args:
257
            action_dict (dict): Action attributes.
258
        """
259 1
        action_type = action_dict.get('action_type')
260 1
        action_class = cls._action_class[action_type]
261 1
        return action_class.from_dict(action_dict) if action_class else None
262
263 1
    @classmethod
264
    def from_of_action(cls, of_action):
265
        """Build the proper Action from a pyof action.
266
267
        Args:
268
            of_action (pyof action): Action from python-openflow.
269
        """
270
        of_class = type(of_action)
271
        action_class = cls._action_class.get(of_class)
272
        return action_class.from_of_action(of_action) if action_class else None
273
274
275 1
class MatchBase:  # pylint: disable=too-many-instance-attributes
276
    """Base class with common high-level Match fields."""
277
278 1
    def __init__(self, in_port=None, dl_src=None, dl_dst=None, dl_vlan=None,
279
                 dl_vlan_pcp=None, dl_type=None, nw_proto=None, nw_src=None,
280
                 nw_dst=None, tp_src=None, tp_dst=None):
281
        """Make it possible to set all attributes from the constructor."""
282
        # pylint: disable=too-many-arguments
283 1
        self.in_port = in_port
284 1
        self.dl_src = dl_src
285 1
        self.dl_dst = dl_dst
286 1
        self.dl_vlan = dl_vlan
287 1
        self.dl_vlan_pcp = dl_vlan_pcp
288 1
        self.dl_type = dl_type
289 1
        self.nw_proto = nw_proto
290 1
        self.nw_src = nw_src
291 1
        self.nw_dst = nw_dst
292 1
        self.tp_src = tp_src
293 1
        self.tp_dst = tp_dst
294
295 1
    def as_dict(self):
296
        """Return a dictionary excluding ``None`` values."""
297 1
        return {k: v for k, v in self.__dict__.items() if v is not None}
298
299 1
    @classmethod
300
    def from_dict(cls, match_dict):
301
        """Return a Match instance from a dictionary."""
302 1
        match = cls()
303 1
        for key, value in match_dict.items():
304 1
            if key in match.__dict__:
305 1
                setattr(match, key, value)
306 1
        return match
307
308 1
    @classmethod
309 1
    @abstractmethod
310
    def from_of_match(cls, of_match):
311
        """Return a Match instance from a pyof Match."""
312
        pass
313
314 1
    @abstractmethod
315
    def as_of_match(self):
316
        """Return a python-openflow Match."""
317
        pass
318
319
320 1
class Stats:
321
    """Simple class to store statistics as attributes and values."""
322
323 1
    def as_dict(self):
324
        """Return a dict excluding attributes with ``None`` value."""
325 1
        return {attribute: value
326
                for attribute, value in vars(self).items()
327
                if value is not None}
328
329 1
    @classmethod
330
    def from_dict(cls, stats_dict):
331
        """Return a statistics object from a dictionary."""
332 1
        stats = cls()
333 1
        cls._update(stats, stats_dict.items())
334 1
        return stats
335
336 1
    @classmethod
337
    def from_of_flow_stats(cls, of_stats):
338
        """Create an instance from a pyof FlowStats."""
339
        stats = cls()
340
        stats.update(of_stats)
341
        return stats
342
343 1
    def update(self, of_stats):
344
        """Given a pyof stats object, update attributes' values.
345
346
        Avoid object creation and memory leak. pyof values are GenericType
347
        instances whose native values can be accessed by `.value`.
348
        """
349
        # Generator for GenericType values
350
        attr_name_value = ((attr_name, gen_type.value)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable gen_type does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable attr_name does not seem to be defined.
Loading history...
351
                           for attr_name, gen_type in vars(of_stats).items()
352
                           if attr_name in vars(self))
353
        self._update(self, attr_name_value)
354
355 1
    @staticmethod
356
    def _update(obj, iterable):
357
        """From attribute name and value pairs, update ``obj``."""
358 1
        for attr_name, value in iterable:
359
            if hasattr(obj, attr_name):
360
                setattr(obj, attr_name, value)
361
362
363 1
class FlowStats(Stats):
364
    """Common fields for 1.0 and 1.3 FlowStats."""
365
366 1
    def __init__(self):
367
        """Initialize all statistics as ``None``."""
368 1
        self.byte_count = None
369 1
        self.duration_sec = None
370 1
        self.duration_nsec = None
371 1
        self.packet_count = None
372
373
374 1
class PortStats(Stats):  # pylint: disable=too-many-instance-attributes
375
    """Common fields for 1.0 and 1.3 PortStats."""
376
377 1
    def __init__(self):
378
        """Initialize all statistics as ``None``."""
379
        self.rx_packets = None
380
        self.tx_packets = None
381
        self.rx_bytes = None
382
        self.tx_bytes = None
383
        self.rx_dropped = None
384
        self.tx_dropped = None
385
        self.rx_errors = None
386
        self.tx_errors = None
387
        self.rx_frame_err = None
388
        self.rx_over_err = None
389
        self.rx_crc_err = None
390
        self.collisions = None
391