Test Failed
Pull Request — master (#102)
by Carlos
04:39
created

build.flow.MatchBase.__init__()   B

Complexity

Conditions 1

Size

Total Lines 54
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 51
dl 0
loc 54
ccs 45
cts 45
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=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)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable of_action does not seem to be defined.
Loading history...
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
    def __eq__(self, other):
213 1
        include_id = False
214
        if not isinstance(other, self.__class__):
215
            raise ValueError(f'Error comparing two flows, the {other}'
216 1
                             'is not a FlowBase class')
217
218 1
        if self.as_dict(include_id) == other.as_dict(include_id):
219
            return True
220 1
        return False
221
222
223 1
class ActionBase(ABC):
224 1
    """Base class for a flow action."""
225 1
226 1
    def as_dict(self):
227 1
        """Return a dict that can be dumped as JSON."""
228
        return vars(self)
229 1
230
    @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
            if hasattr(action, attr_name):
236
                setattr(action, attr_name, value)
237
        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
    @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
    _action_class = {
254
        'output': None,
255
        'set_vlan': None,
256 1
        # pyof class: ActionBase child
257 1
    }
258 1
259
    @classmethod
260 1
    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
        action_type = action_dict.get('action_type')
267
        action_class = cls._action_class[action_type]
268
        return action_class.from_dict(action_dict) if action_class else None
269
270
    @classmethod
271
    def from_of_action(cls, of_action):
272 1
        """Build the proper Action from a pyof action.
273
274
        Args:
275 1
            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
class MatchBase:  # pylint: disable=too-many-instance-attributes
283
    """Base class with common high-level Match fields."""
284
285
    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 1
                 sctp_src=None, sctp_dst=None, icmpv4_type=None,
290 1
                 icmpv4_code=None, arp_op=None, arp_spa=None, arp_tpa=None,
291 1
                 arp_sha=None, arp_tha=None, ipv6_src=None, ipv6_dst=None,
292 1
                 ipv6_flabel=None, icmpv6_type=None, icmpv6_code=None,
293 1
                 nd_tar=None, nd_sll=None, nd_tll=None, mpls_lab=None,
294 1
                 mpls_tc=None, mpls_bos=None, pbb_isid=None, v6_hdr=None,
295 1
                 metadata=None, tun_id=None):
296 1
        """Make it possible to set all attributes from the constructor."""
297 1
        # pylint: disable=too-many-arguments
298 1
        # 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
        self.nd_tar = nd_tar
330 1
        self.nd_sll = nd_sll
331
        self.nd_tll = nd_tll
332 1
        self.mpls_lab = mpls_lab
333
        self.mpls_tc = mpls_tc
334 1
        self.mpls_bos = mpls_bos
335
        self.pbb_isid = pbb_isid
336
        self.v6_hdr = v6_hdr
337 1
        self.metadata = metadata
338 1
        self.tun_id = tun_id
339 1
340 1
    def as_dict(self):
341 1
        """Return a dictionary excluding ``None`` values."""
342
        return {k: v for k, v in self.__dict__.items() if v is not None}
343 1
344 1
    @classmethod
345
    def from_dict(cls, match_dict):
346
        """Return a Match instance from a dictionary."""
347
        match = cls()
348 1
        for key, value in match_dict.items():
349
            if key in match.__dict__:
350
                setattr(match, key, value)
351
        return match
352
353 1
    @classmethod
354
    @abstractmethod
355
    def from_of_match(cls, of_match):
356 1
        """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 1
363
class Stats:
364
    """Simple class to store statistics as attributes and values."""
365 1
366 1
    def as_dict(self):
367 1
        """Return a dict excluding attributes with ``None`` value."""
368
        return {attribute: value
369 1
                for attribute, value in vars(self).items()
370
                if value is not None}
371
372
    @classmethod
373
    def from_dict(cls, stats_dict):
374
        """Return a statistics object from a dictionary."""
375
        stats = cls()
376 1
        cls._update(stats, stats_dict.items())
377
        return stats
378
379
    @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
    def update(self, of_stats):
387
        """Given a pyof stats object, update attributes' values.
388 1
389
        Avoid object creation and memory leak. pyof values are GenericType
390
        instances whose native values can be accessed by `.value`.
391 1
        """
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 1
        self._update(self, attr_name_value)
397
398
    @staticmethod
399 1
    def _update(obj, iterable):
400
        """From attribute name and value pairs, update ``obj``."""
401 1
        for attr_name, value in iterable:
402 1
            if hasattr(obj, attr_name):
403 1
                setattr(obj, attr_name, value)
404 1
405
406
class FlowStats(Stats):
407 1
    """Common fields for 1.0 and 1.3 FlowStats."""
408
409
    def __init__(self):
410 1
        """Initialize all statistics as ``None``."""
411
        self.byte_count = None
412
        self.duration_sec = None
413
        self.duration_nsec = None
414
        self.packet_count = None
415
416
417
class PortStats(Stats):  # pylint: disable=too-many-instance-attributes
418
    """Common fields for 1.0 and 1.3 PortStats."""
419
420
    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