Passed
Push — master ( d84e35...e2f2a2 )
by Humberto
01:40 queued 11s
created

build.flow   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Test Coverage

Coverage 86.43%

Importance

Changes 0
Metric Value
wmc 50
eloc 254
dl 0
loc 432
ccs 172
cts 199
cp 0.8643
rs 8.4
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A FlowBase.__init__() 0 28 1
A FlowBase.id() 0 16 1
A FlowBase.as_dict() 0 28 2
B FlowBase.from_dict() 0 25 8
A FlowBase.as_json() 0 14 1
A FlowBase._get_of_actions() 0 4 1
A FlowBase.from_of_flow_stats() 0 16 1
A FlowBase._as_of_flow_mod() 0 12 1
A FlowBase.as_of_add_flow_mod() 0 3 1
A FlowBase.as_of_delete_flow_mod() 0 3 1
A FlowFactory.get_class() 0 9 3
A FlowFactory.from_of_flow_stats() 0 5 1
A ActionBase.as_of_action() 0 3 1
A ActionFactoryBase.from_of_action() 0 10 2
A ActionFactoryBase.from_dict() 0 10 2
A Stats.update() 0 11 1
A MatchBase.as_dict() 0 3 1
A ActionBase.as_dict() 0 3 1
A Stats.as_dict() 0 5 1
A MatchBase.as_of_match() 0 3 1
A Stats._update() 0 6 3
A Stats.from_of_flow_stats() 0 6 1
A MatchBase.from_dict() 0 8 3
A PortStats.__init__() 0 14 1
A ActionBase.from_of_action() 0 4 1
B MatchBase.__init__() 0 54 1
A Stats.from_dict() 0 6 1
A ActionBase.from_dict() 0 8 3
A FlowStats.__init__() 0 6 1
A FlowBase.__eq__() 0 7 2
A MatchBase.from_of_match() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like build.flow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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