Passed
Push — master ( c3fca6...c0cc54 )
by Humberto
02:28
created

build.flow.MatchBase.__init__()   B

Complexity

Conditions 1

Size

Total Lines 54
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 51
dl 0
loc 54
ccs 41
cts 41
cp 1
rs 8.6036
c 0
b 0
f 0
cc 1
nop 41
crap 1

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 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
        return json.dumps(self.as_dict(include_id), sort_keys=sort_keys)
169
170 1
    def as_of_add_flow_mod(self):
171
        """Return an OpenFlow add FlowMod."""
172 1
        return self._as_of_flow_mod(FlowModCommand.OFPFC_ADD)
173
174 1
    def as_of_delete_flow_mod(self):
175
        """Return an OpenFlow delete FlowMod."""
176 1
        return self._as_of_flow_mod(FlowModCommand.OFPFC_DELETE)
177
178 1
    @abstractmethod
179
    def _as_of_flow_mod(self, command):
180
        """Return a pyof FlowMod with given ``command``."""
181
        # Disable not-callable error as subclasses will set a class
182 1
        flow_mod = self._flow_mod_class()  # pylint: disable=E1102
183 1
        flow_mod.match = self.match.as_of_match()
184 1
        flow_mod.cookie = self.cookie
185 1
        flow_mod.command = command
186 1
        flow_mod.idle_timeout = self.idle_timeout
187 1
        flow_mod.hard_timeout = self.hard_timeout
188 1
        flow_mod.priority = self.priority
189 1
        flow_mod.table_id = self.table_id
190 1
        return flow_mod
191
192 1
    @staticmethod
193 1
    @abstractmethod
194
    def _get_of_actions(of_flow_stats):
195
        """Return pyof actions from pyof FlowStats."""
196
197 1
    @classmethod
198
    def from_of_flow_stats(cls, of_flow_stats, switch):
199
        """Create a flow with latest stats based on pyof FlowStats."""
200
        of_actions = cls._get_of_actions(of_flow_stats)
201
        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...
202
                   for of_action in of_actions)
203
        non_none_actions = [action for action in actions if action]
204
        return cls(switch,
205
                   table_id=of_flow_stats.table_id.value,
206
                   match=cls._match_class.from_of_match(of_flow_stats.match),
207
                   priority=of_flow_stats.priority.value,
208
                   idle_timeout=of_flow_stats.idle_timeout.value,
209
                   hard_timeout=of_flow_stats.hard_timeout.value,
210
                   cookie=of_flow_stats.cookie.value,
211
                   actions=non_none_actions,
212
                   stats=FlowStats.from_of_flow_stats(of_flow_stats))
213
214 1
    def __eq__(self, other):
215 1
        include_id = False
216 1
        if not isinstance(other, self.__class__):
217 1
            raise ValueError(f'Error comparing flows: {other} is not '
218
                             f'an instance of {self.__class__}')
219
220 1
        return self.as_dict(include_id) == other.as_dict(include_id)
221
222
223 1
class ActionBase(ABC):
224
    """Base class for a flow action."""
225
226 1
    def as_dict(self):
227
        """Return a dict that can be dumped as JSON."""
228 1
        return vars(self)
229
230 1
    @classmethod
231
    def from_dict(cls, action_dict):
232
        """Return an action instance from attributes in a dictionary."""
233 1
        action = cls(None)
234 1
        for attr_name, value in action_dict.items():
235 1
            if hasattr(action, attr_name):
236 1
                setattr(action, attr_name, value)
237 1
        return action
238
239 1
    @abstractmethod
240
    def as_of_action(self):
241
        """Return a pyof action to be used by a FlowMod."""
242
243 1
    @classmethod
244 1
    @abstractmethod
245
    def from_of_action(cls, of_action):
246
        """Return an action from a pyof action."""
247
248
249 1
class ActionFactoryBase(ABC):
250
    """Deal with different implementations of ActionBase."""
251
252
    # key: action_type or pyof class, value: ActionBase child
253 1
    _action_class = {
254
        'output': None,
255
        'set_vlan': None,
256
        # pyof class: ActionBase child
257
    }
258
259 1
    @classmethod
260
    def from_dict(cls, action_dict):
261
        """Build the proper Action from a dictionary.
262
263
        Args:
264
            action_dict (dict): Action attributes.
265
        """
266 1
        action_type = action_dict.get('action_type')
267 1
        action_class = cls._action_class[action_type]
268 1
        return action_class.from_dict(action_dict) if action_class else None
269
270 1
    @classmethod
271
    def from_of_action(cls, of_action):
272
        """Build the proper Action from a pyof action.
273
274
        Args:
275
            of_action (pyof action): Action from python-openflow.
276
        """
277
        of_class = type(of_action)
278
        action_class = cls._action_class.get(of_class)
279
        return action_class.from_of_action(of_action) if action_class else None
280
281
282 1
class MatchBase:  # pylint: disable=too-many-instance-attributes
283
    """Base class with common high-level Match fields."""
284
285 1
    def __init__(self, in_port=None, dl_src=None, dl_dst=None, dl_vlan=None,
286
                 dl_vlan_pcp=None, dl_type=None, nw_proto=None, nw_src=None,
287
                 nw_dst=None, tp_src=None, tp_dst=None, in_phy_port=None,
288
                 ip_dscp=None, ip_ecn=None, udp_src=None, udp_dst=None,
289
                 sctp_src=None, sctp_dst=None, icmpv4_type=None,
290
                 icmpv4_code=None, arp_op=None, arp_spa=None, arp_tpa=None,
291
                 arp_sha=None, arp_tha=None, ipv6_src=None, ipv6_dst=None,
292
                 ipv6_flabel=None, icmpv6_type=None, icmpv6_code=None,
293
                 nd_tar=None, nd_sll=None, nd_tll=None, mpls_lab=None,
294
                 mpls_tc=None, mpls_bos=None, pbb_isid=None, v6_hdr=None,
295
                 metadata=None, tun_id=None):
296
        """Make it possible to set all attributes from the constructor."""
297
        # pylint: disable=too-many-arguments
298
        # pylint: disable=too-many-locals
299 1
        self.in_port = in_port
300 1
        self.dl_src = dl_src
301 1
        self.dl_dst = dl_dst
302 1
        self.dl_vlan = dl_vlan
303 1
        self.dl_vlan_pcp = dl_vlan_pcp
304 1
        self.dl_type = dl_type
305 1
        self.nw_proto = nw_proto
306 1
        self.nw_src = nw_src
307 1
        self.nw_dst = nw_dst
308 1
        self.tp_src = tp_src
309 1
        self.tp_dst = tp_dst
310 1
        self.in_phy_port = in_phy_port
311 1
        self.ip_dscp = ip_dscp
312 1
        self.ip_ecn = ip_ecn
313 1
        self.udp_src = udp_src
314 1
        self.udp_dst = udp_dst
315 1
        self.sctp_src = sctp_src
316 1
        self.sctp_dst = sctp_dst
317 1
        self.icmpv4_type = icmpv4_type
318 1
        self.icmpv4_code = icmpv4_code
319 1
        self.arp_op = arp_op
320 1
        self.arp_spa = arp_spa
321 1
        self.arp_tpa = arp_tpa
322 1
        self.arp_sha = arp_sha
323 1
        self.arp_tha = arp_tha
324 1
        self.ipv6_src = ipv6_src
325 1
        self.ipv6_dst = ipv6_dst
326 1
        self.ipv6_flabel = ipv6_flabel
327 1
        self.icmpv6_type = icmpv6_type
328 1
        self.icmpv6_code = icmpv6_code
329 1
        self.nd_tar = nd_tar
330 1
        self.nd_sll = nd_sll
331 1
        self.nd_tll = nd_tll
332 1
        self.mpls_lab = mpls_lab
333 1
        self.mpls_tc = mpls_tc
334 1
        self.mpls_bos = mpls_bos
335 1
        self.pbb_isid = pbb_isid
336 1
        self.v6_hdr = v6_hdr
337 1
        self.metadata = metadata
338 1
        self.tun_id = tun_id
339
340 1
    def as_dict(self):
341
        """Return a dictionary excluding ``None`` values."""
342 1
        return {k: v for k, v in self.__dict__.items() if v is not None}
343
344 1
    @classmethod
345
    def from_dict(cls, match_dict):
346
        """Return a Match instance from a dictionary."""
347 1
        match = cls()
348 1
        for key, value in match_dict.items():
349 1
            if key in match.__dict__:
350 1
                setattr(match, key, value)
351 1
        return match
352
353 1
    @classmethod
354 1
    @abstractmethod
355
    def from_of_match(cls, of_match):
356
        """Return a Match instance from a pyof Match."""
357
358 1
    @abstractmethod
359
    def as_of_match(self):
360
        """Return a python-openflow Match."""
361
362
363 1
class Stats:
364
    """Simple class to store statistics as attributes and values."""
365
366 1
    def as_dict(self):
367
        """Return a dict excluding attributes with ``None`` value."""
368 1
        return {attribute: value
369
                for attribute, value in vars(self).items()
370
                if value is not None}
371
372 1
    @classmethod
373
    def from_dict(cls, stats_dict):
374
        """Return a statistics object from a dictionary."""
375 1
        stats = cls()
376 1
        cls._update(stats, stats_dict.items())
377 1
        return stats
378
379 1
    @classmethod
380
    def from_of_flow_stats(cls, of_stats):
381
        """Create an instance from a pyof FlowStats."""
382
        stats = cls()
383
        stats.update(of_stats)
384
        return stats
385
386 1
    def update(self, of_stats):
387
        """Given a pyof stats object, update attributes' values.
388
389
        Avoid object creation and memory leak. pyof values are GenericType
390
        instances whose native values can be accessed by `.value`.
391
        """
392
        # Generator for GenericType values
393
        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...
394
                           for attr_name, gen_type in vars(of_stats).items()
395
                           if attr_name in vars(self))
396
        self._update(self, attr_name_value)
397
398 1
    @staticmethod
399
    def _update(obj, iterable):
400
        """From attribute name and value pairs, update ``obj``."""
401 1
        for attr_name, value in iterable:
402
            if hasattr(obj, attr_name):
403
                setattr(obj, attr_name, value)
404
405
406 1
class FlowStats(Stats):
407
    """Common fields for 1.0 and 1.3 FlowStats."""
408
409 1
    def __init__(self):
410
        """Initialize all statistics as ``None``."""
411 1
        self.byte_count = None
412 1
        self.duration_sec = None
413 1
        self.duration_nsec = None
414 1
        self.packet_count = None
415
416
417 1
class PortStats(Stats):  # pylint: disable=too-many-instance-attributes
418
    """Common fields for 1.0 and 1.3 PortStats."""
419
420 1
    def __init__(self):
421
        """Initialize all statistics as ``None``."""
422
        self.rx_packets = None
423
        self.tx_packets = None
424
        self.rx_bytes = None
425
        self.tx_bytes = None
426
        self.rx_dropped = None
427
        self.tx_dropped = None
428
        self.rx_errors = None
429
        self.tx_errors = None
430
        self.rx_frame_err = None
431
        self.rx_over_err = None
432
        self.rx_crc_err = None
433
        self.collisions = None
434