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