Passed
Pull Request — master (#128)
by Carlos
03:05
created

build.flow.FlowBase.__init__()   A

Complexity

Conditions 1

Size

Total Lines 29
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 29
ccs 10
cts 10
cp 1
rs 9.8
c 0
b 0
f 0
cc 1
nop 10
crap 1

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