-
Notifications
You must be signed in to change notification settings - Fork 191
/
fakeoftable.py
1293 lines (1158 loc) · 49.9 KB
/
fakeoftable.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Manage Fake OF tables for unit tests"""
# pylint: disable=too-many-lines
# Copyright (C) 2015 Research and Innovation Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import argparse
import json
import ast
import heapq
import pprint
from collections import OrderedDict
from bitstring import Bits
from os_ken.ofproto import ofproto_v1_3 as ofp
from os_ken.ofproto import ofproto_v1_3_parser as parser
from os_ken.ofproto import ofproto_parser as ofp_parser
from os_ken.lib import addrconv
CONTROLLER_PORT = 4294967293
IN_PORT = 4294967288
class FakeOFTableException(Exception):
"""Indicates an erroneous flow or group mod"""
class DFS:
"""Provides a way of tracking the search through the FakeOFNetwork"""
visited = None
heap = None
def __init__(self):
self.visited = {}
self.heap = []
def visit(self, dp_id, pkt):
"""
Notifies the DFS that a packet has visited the dp_id
Args:
dp_id: The DP ID for the node that is being visited
pkt: The packet that is visiting the node
"""
self.visited.setdefault(dp_id, [])
if pkt not in self.visited[dp_id]:
self.visited[dp_id].append(pkt)
def has_visited(self, dp_id, pkt):
"""
Returns true if the packet has visited the node DP ID before
Args:
dp_id: The DP ID for the node is being visited
pkt: The packet that is visiting the node
"""
if dp_id in self.visited:
if pkt in self.visited[dp_id]:
return True
return False
def peek(self):
"""
Returns the first item in the heap (with the highest priority (smallest value))
with popping from the heap
Returns:
dp_id, pkt
"""
if not self.heap:
return None, None
item = self.heap[0]
return item[1][0], item[1][1]
def push(self, dp_id, pkt, priority):
"""
Pushes the dp_id and pkt onto the heap with priority
Args:
dp_id:
pkt:
priority:
"""
heapq.heappush(self.heap, (priority, (dp_id, tuple(pkt.items()))))
def pop(self):
"""
Obtains the item with the highest priority
Returns:
dp_id, pkt
"""
if not self.heap:
return None, None
item = heapq.heappop(self.heap)
return item[1][0], item[1][1]
class FakeOFNetwork:
"""
FakeOFNetwork is a virtual openflow pipeline used for testing openflow controllers
The network contains multiple FakeOFTables to represent multiple switches in a network
"""
def __init__(self, valves_manager, num_tables, requires_tfm=True):
"""
Args:
valves_manager (ValvesManager): Valves manager class to resolve stack traversals
num_tables (int): The number of tables to configure in each FakeOFTable
requires_tfm (bool): Whether TFMs are required
"""
self.valves_manager = valves_manager
self.tables = {}
for dp_id in self.valves_manager.valves:
self.tables[dp_id] = FakeOFTable(dp_id, num_tables, requires_tfm)
def apply_ofmsgs(self, dp_id, ofmsgs, ignore_errors=False):
"""Applies ofmsgs to a FakeOFTable for DP ID"""
self.tables[dp_id].apply_ofmsgs(ofmsgs, ignore_errors=ignore_errors)
def print_table(self, dp_id):
"""Prints the table in string format to STDERR"""
sys.stderr.write("TABLE %x" % dp_id)
sys.stderr.write(str(self.tables[dp_id]) + "\n")
sys.stderr.write("======================\n\n")
def shortest_path_len(self, src_dpid, dst_dpid):
"""Returns the length of the shortest path from the source to the destination"""
if src_dpid == dst_dpid:
return 1
src_valve = self.valves_manager.valves[src_dpid]
dst_valve = self.valves_manager.valves[dst_dpid]
if src_valve.dp.stack and dst_valve.dp.stack:
return len(src_valve.dp.stack.shortest_path(dst_valve.dp.name))
return 2
def is_output(self, match, src_dpid, dst_dpid, port=None, vid=None, trace=False):
"""
Traverses a packet through the network until we have searched everything
or successfully output a packet to the destination with expected port and vid
If port is None return True if output to any port (including special ports)
regardless of VLAN tag.
If vid is None return True if output to specified port regardless of VLAN tag.
If vid OFPVID_PRESENT bit is 0, return True if output packet does not have
a VLAN tag OR packet OFPVID_PRESENT is 0
Args:
match (dict): A dictionary keyed by header field names with values
src_dpid: The source DP ID of the match packet entering the Fake OF network
dst_dpid: The expected destination DP ID of the packet match
port: The expected output port on the destination DP
vid: The expected output vid on the destination DP
trace (bool): Print the trace of traversing the tables
Returns:
true if packets with match fields is output to port with correct VLAN
"""
found = False
dfs = DFS()
priority = self.shortest_path_len(src_dpid, dst_dpid)
pkt = match.copy()
dfs.push(src_dpid, pkt, priority)
dfs.visit(src_dpid, pkt)
while not found:
# Search through the packet paths until we have searched everything or
# successfully output the packet to the destination in the expected format
dp_id, pkt = dfs.pop()
if dp_id is None or pkt is None:
break
pkt = dict(pkt)
if dp_id == dst_dpid:
# A packet has reached the destination, so test for the output
found = self.tables[dp_id].is_full_output(pkt, port, vid, trace=trace)
if not found and trace:
# A packet on the destination DP is not output in the expected state so
# continue searching (flood reflection)
sys.stderr.write("Output is away from destination\n")
if not found:
# Packet not reached destination, so continue traversing
if trace:
sys.stderr.write("FakeOFTable %s: %s\n" % (dp_id, pkt))
port_outputs = self.tables[dp_id].get_port_outputs(pkt, trace=trace)
valve = self.valves_manager.valves[dp_id]
for out_port, out_pkts in port_outputs.items():
if out_port == IN_PORT:
# Rebind output to the packet in_port value
out_port = pkt["in_port"]
if out_port not in valve.dp.ports:
# Ignore output to improper ports & controller
# TODO: Here we should actually send the packet to the
# controller, and maybe install necessary rules to
# help testing routing implementations
continue
for out_pkt in out_pkts:
port_obj = valve.dp.ports[out_port]
if port_obj.stack:
# Need to continue traversing through the FakeOFNetwork
adj_port = port_obj.stack["port"]
adj_dpid = port_obj.stack["dp"].dp_id
new_pkt = out_pkt.copy()
new_pkt["in_port"] = adj_port.number
if not dfs.has_visited(adj_dpid, new_pkt):
# Add packet to the heap if we have not visited the node with
# this packet before
priority = self.shortest_path_len(adj_dpid, dst_dpid)
dfs.push(adj_dpid, new_pkt, priority)
dfs.visit(adj_dpid, new_pkt)
elif trace:
# Output to non-stack port, can ignore this output
sys.stderr.write(
"Ignoring non-stack output %s:%s\n"
% (valve.dp.name, out_port)
)
if trace:
sys.stderr.write("\n")
return found
def table_state(self, dp_id):
"""Return tuple of table hash & table str"""
return self.tables[dp_id].table_state()
def hash_table(self, dp_id):
"""Return a hash of a single FakeOFTable"""
return self.tables[dp_id].__hash__()
class FakeOFTable:
"""Fake OFTable is a virtual openflow pipeline used for testing openflow
controllers.
The tables are populated using apply_ofmsgs and can be queried with
is_output.
"""
def __init__(self, dp_id, num_tables=1, requires_tfm=True):
self.dp_id = dp_id
self.tables = [[] for _ in range(0, num_tables)]
self.groups = {}
self.requires_tfm = requires_tfm
self.tfm = {}
def table_state(self):
"""Return tuple of table hash & table str"""
table_str = str(self.tables)
return (hash(frozenset(table_str)), table_str)
def __hash__(self):
"""Return a host of the tables"""
return hash(frozenset(str(self.tables)))
def _apply_groupmod(self, ofmsg):
"""Maintain group table."""
def _del(_ofmsg, group_id):
if group_id == ofp.OFPG_ALL:
self.groups = {}
return
if group_id in self.groups:
del self.groups[group_id]
def _add(ofmsg, group_id):
if group_id in self.groups:
raise FakeOFTableException("group already in group table: %s" % ofmsg)
self.groups[group_id] = ofmsg
def _modify(ofmsg, group_id):
if group_id not in self.groups:
raise FakeOFTableException("group not in group table: %s" % ofmsg)
self.groups[group_id] = ofmsg
_groupmod_handlers = {
ofp.OFPGC_DELETE: _del,
ofp.OFPGC_ADD: _add,
ofp.OFPGC_MODIFY: _modify,
}
_groupmod_handlers[ofmsg.command](ofmsg, ofmsg.group_id)
def _apply_flowmod(self, ofmsg):
"""Adds, Deletes and modify flow modification messages are applied
according to section 6.4 of the OpenFlow 1.3 specification."""
def _validate_flowmod_tfm(table_id, tfm_body, ofmsg):
if not self.requires_tfm:
return
if table_id == ofp.OFPTT_ALL:
if ofmsg.match.items() and not self.tfm:
raise FakeOFTableException(
"got %s with matches before TFM that defines tables" % ofmsg
)
return
if tfm_body is None:
raise FakeOFTableException(
"got %s before TFM that defines table %u" % (ofmsg, table_id)
)
def _add(table, flowmod):
# From the 1.3 spec, section 6.4:
# For add requests (OFPFC_ADD) with the
# OFPFF_CHECK_OVERLAP flag set, the switch must first
# check for any overlapping flow entries in the
# requested table. Two flow entries overlap if a
# single packet may match both, and both flow entries
# have the same priority, but the two flow entries
# don't have the exact same match. If an overlap
# conflict exists between an existing flow entry and
# the add request, the switch must refuse the addition
# and respond with an ofp_error_msg with
# OFPET_FLOW_MOD_FAILED type and OFPFMFC_OVERLAP code.
#
# Without the check overlap flag it seems like it is
# possible that we can have overlapping flow table
# entries which will cause ambiguous behaviour. This is
# obviously unnacceptable so we will assume this is
# always set
for fte in table:
if flowmod.fte_matches(fte, strict=True):
table.remove(fte)
break
if flowmod.overlaps(fte):
raise FakeOFTableException(
"Overlapping flowmods {} and {}".format(flowmod, fte)
)
table.append(flowmod)
def _del(table, flowmod):
removals = [fte for fte in table if flowmod.fte_matches(fte)]
for fte in removals:
table.remove(fte)
def _del_strict(table, flowmod):
for fte in table:
if flowmod.fte_matches(fte, strict=True):
table.remove(fte)
break
def _modify(table, flowmod):
for fte in table:
if flowmod.fte_matches(fte):
fte.instructions = flowmod.instructions
def _modify_strict(table, flowmod):
for fte in table:
if flowmod.fte_matches(fte, strict=True):
fte.instructions = flowmod.instructions
break
_flowmod_handlers = {
ofp.OFPFC_ADD: _add,
ofp.OFPFC_DELETE: _del,
ofp.OFPFC_DELETE_STRICT: _del_strict,
ofp.OFPFC_MODIFY: _modify,
ofp.OFPFC_MODIFY_STRICT: _modify_strict,
}
table_id = ofmsg.table_id
tfm_body = self.tfm.get(table_id, None)
if table_id == ofp.OFPTT_ALL or table_id is None:
tables = self.tables
else:
tables = [self.tables[table_id]]
_validate_flowmod_tfm(table_id, tfm_body, ofmsg)
flowmod = FlowMod(ofmsg)
for table in tables:
_flowmod_handlers[ofmsg.command](table, flowmod)
if tfm_body:
for table in tables:
entries = len(table)
if entries > tfm_body.max_entries:
tfm_table_details = "%s : table %u %s full (%u/%u)" % (
self.dp_id,
table_id,
tfm_body.name,
entries,
tfm_body.max_entries,
)
flow_dump = "\n\n".join(
(tfm_table_details, str(ofmsg), str(tfm_body))
)
raise FakeOFTableException(flow_dump)
def _apply_tfm(self, ofmsg):
self.tfm = {body.table_id: body for body in ofmsg.body}
def _apply_flowstats(self, ofmsg):
"""Update state of flow tables to match an OFPFlowStatsReply message.
This assumes a tfm is not required."""
self.tables = []
self.requires_tfm = False
self.tfm = {}
for stat in ofmsg.body:
while len(self.tables) <= stat.table_id:
self.tables.append([])
self.tables[stat.table_id].append(FlowMod(stat))
def apply_ofmsgs(self, ofmsgs, ignore_errors=False):
"""Update state of test flow tables."""
for ofmsg in ofmsgs:
try:
if isinstance(ofmsg, parser.OFPBarrierRequest):
continue
if isinstance(ofmsg, parser.OFPPacketOut):
continue
if isinstance(ofmsg, parser.OFPSetConfig):
continue
if isinstance(ofmsg, parser.OFPSetAsync):
continue
if isinstance(ofmsg, parser.OFPDescStatsRequest):
continue
if isinstance(ofmsg, parser.OFPMeterMod):
# TODO: handle OFPMeterMod
continue
if isinstance(ofmsg, parser.OFPTableFeaturesStatsRequest):
self._apply_tfm(ofmsg)
continue
if isinstance(ofmsg, parser.OFPGroupMod):
self._apply_groupmod(ofmsg)
continue
if isinstance(ofmsg, parser.OFPFlowMod):
self._apply_flowmod(ofmsg)
self.sort_tables()
continue
if isinstance(ofmsg, parser.OFPFlowStatsReply):
self._apply_flowstats(ofmsg)
self.sort_tables()
continue
except FakeOFTableException:
if not ignore_errors:
raise
if not ignore_errors:
raise FakeOFTableException("Unsupported flow %s" % str(ofmsg))
def single_table_lookup(self, match, table_id, trace=False):
"""
Searches through a single table with `table_id` for entries
that will be applied to the packet with fields represented by match
Args:
match (dict): A dictionary keyed by header field names with values
table_id (int): The table ID to send the match packet through
trace (bool): Print the trace of traversing the table
Returns:
matching_fte: First matching flowmod in the table
"""
packet_dict = match.copy()
table = self.tables[table_id]
matching_fte = None
# Find matching flowmods
for fte in table:
if fte.pkt_matches(packet_dict):
matching_fte = fte
break
if trace:
sys.stderr.write("%s: %s\n" % (table_id, matching_fte))
return matching_fte
def _process_instruction(self, match, instruction):
"""
Process an instructions actions into an output dictionary
Args:
match (dict): A dictionary keyed by header field names with values
instruction: The instruction being applied to the packet match
Returns:
outputs: OrderedDict of an output port to list of output packets
packet_dict: final dictionary of the packet
"""
outputs = OrderedDict()
packet_dict = match.copy()
pending_actions = []
for action in instruction.actions:
if action.type == ofp.OFPAT_OUTPUT:
# Save the packet that is output to a port
outputs.setdefault(action.port, [])
outputs[action.port].append(packet_dict.copy())
pending_actions = []
continue
pending_actions.append(action)
if action.type == ofp.OFPAT_SET_FIELD:
# Set field, modify a packet header
packet_dict[action.key] = action.value
elif action.type == ofp.OFPAT_PUSH_VLAN:
if (
"vlan_vid" in packet_dict
and packet_dict["vlan_vid"] & ofp.OFPVID_PRESENT
):
# Pushing on another tag, so create another
# field for the encapsulated VID
packet_dict["encap_vid"] = packet_dict["vlan_vid"]
# Push the VLAN header to the packet
packet_dict["vlan_vid"] = ofp.OFPVID_PRESENT
elif action.type == ofp.OFPAT_POP_VLAN:
# Remove VLAN header from the packet
packet_dict.pop("vlan_vid")
if "vlan_pcp" in packet_dict:
# Also make sure to pop off any VLAN header information too
packet_dict.pop("vlan_pcp")
if "encap_vid" in packet_dict:
# Move the encapsulated VID to the front
packet_dict["vlan_vid"] = packet_dict["encap_vid"]
packet_dict.pop("encap_vid")
else:
packet_dict["vlan_vid"] = 0
elif action.type == ofp.OFPAT_GROUP:
# Group mod so make sure that we process the group buckets
if action.group_id not in self.groups:
raise FakeOFTableException(
"output group not in group table: %s" % action
)
buckets = self.groups[action.group_id].buckets
for bucket in buckets:
bucket_outputs, _, _ = self._process_instruction(
packet_dict, bucket
)
for out_port, out_pkts in bucket_outputs.items():
outputs.setdefault(out_port, [])
outputs[out_port].extend(out_pkts)
pending_actions = []
return outputs, packet_dict, pending_actions
def get_table_output(self, match, table_id, trace=False):
"""
Send a packet through a single table and return the output
ports mapped to the output packet
Args:
match (dict): A dictionary keyed by header field names with values
table_id (int): The table ID to send the packet match through
trace (bool): Print the trace of traversing the table
Returns:
outputs: OrderedDict of an output port to output packet map
packet_dict: The last version of the packet
next_table: Table ID of the next table
"""
next_table = None
packet_dict = match.copy()
outputs = OrderedDict()
matching_fte = self.single_table_lookup(match, table_id, trace)
pending_actions = []
if matching_fte:
for instruction in matching_fte.instructions:
if instruction.type == ofp.OFPIT_GOTO_TABLE:
if table_id < instruction.table_id:
next_table = instruction.table_id
else:
raise FakeOFTableException("goto to lower table ID")
elif instruction.type == ofp.OFPIT_APPLY_ACTIONS:
if not instruction.actions:
raise FakeOFTableException("no-op instruction actions")
(
instruction_outputs,
packet_dict,
pending_actions,
) = self._process_instruction(packet_dict, instruction)
for out_port, out_pkts in instruction_outputs.items():
outputs.setdefault(out_port, [])
outputs[out_port].extend(out_pkts)
elif instruction.type == ofp.OFPIT_WRITE_METADATA:
metadata = packet_dict.get("metadata", 0)
mask = instruction.metadata_mask
mask_compl = mask ^ 0xFFFFFFFFFFFFFFFF
packet_dict["metadata"] = (metadata & mask_compl) | (
instruction.metadata & mask
)
if next_table:
pending_actions = []
if pending_actions:
raise FakeOFTableException(
"flow performs actions on packet after \
output with no goto: %s"
% matching_fte
)
return outputs, packet_dict, next_table
def get_output(self, match, trace=False):
"""
Get all of the outputs of the tables with the output packets
for each table in the FakeOFTable that match progresses through
Args:
match (dict): A dictionary keyed by header field names with values
trace (bool): Print the trace of traversing the table
Returns:
table_outputs: map from table_id output to output ports & packets
for that table
"""
table_outputs = {}
table_id = 0
next_table = True
packet_dict = match.copy()
while next_table:
next_table = False
outputs, packet_dict, next_table_id = self.get_table_output(
packet_dict, table_id, trace
)
table_outputs[table_id] = outputs
next_table = next_table_id is not None
table_id = next_table_id
return table_outputs
def get_port_outputs(self, match, trace=False):
"""
Get all of the outputs of the tables with the output packets
for each table in the FakeOFTable that match progresses through
Args:
match (dict): A dictionary keyed by header field names with value
trace (bool): Print the trace of traversing the table
Returns:
table_outputs: Map from output port number to a list of unique output packets
"""
port_outputs = {}
table_id = 0
next_table = True
packet_dict = match.copy()
while next_table:
next_table = False
outputs, packet_dict, next_table_id = self.get_table_output(
packet_dict, table_id, trace
)
for out_port, out_pkts in outputs.items():
port_outputs.setdefault(out_port, [])
# Remove duplicate entries from the list
for out_pkt in out_pkts:
if out_pkt not in port_outputs[out_port]:
port_outputs[out_port].append(out_pkt)
next_table = next_table_id is not None
table_id = next_table_id
return port_outputs
def is_full_output(self, match, port=None, vid=None, trace=False):
"""
If port is None return True if output to any port (including special ports)
regardless of VLAN tag.
If vid is None return True if output to specified port regardless of VLAN tag.
If vid OFPVID_PRESENT bit is 0, return True if output packet does not have
a VLAN tag OR packet OFPVID_PRESENT is 0
Args:
match (dict): A dictionary keyed by header field names with values
port: The expected output port
vid: The expected output vid
trace (bool): Print the trace of traversing the tables
Returns:
true if packets with match fields is output to port with correct VLAN
"""
table_outputs = self.get_output(match, trace)
if trace:
sys.stderr.write(pprint.pformat(table_outputs) + "\n")
in_port = match.get("in_port")
for table_outputs in table_outputs.values():
for out_port, out_pkts in table_outputs.items():
for out_pkt in out_pkts:
if port == out_port and port == out_pkt["in_port"]:
continue
if port is None:
# Port is None & outputting so return true
return True
if vid is None:
# Vid is None, return true if output to specified port
if port == out_port:
return True
if out_port == ofp.OFPP_IN_PORT and port == in_port:
# In some cases we want to match to specifically ofp.OFPP_IN_PORT
# otherwise we treat ofp.OFPP_IN_PORT as the match in_port
return True
if port == out_port or (
out_port == ofp.OFPP_IN_PORT and port == in_port
):
# Matching port, so check matching VID
if vid & ofp.OFPVID_PRESENT == 0:
# If OFPVID_PRESENT bit is 0 then packet should not have a VLAN tag
return (
"vlan_vid" not in out_pkt
or out_pkt["vlan_vid"] & ofp.OFPVID_PRESENT == 0
)
# VID specified, check if matching expected
return "vlan_vid" in out_pkt and vid == out_pkt["vlan_vid"]
return False
def lookup(self, match, trace=False):
"""Return the entries from flowmods that matches match.
Searches each table in the pipeline for the entries that will be
applied to the packet with fields represented by match.
Arguments:
match: a dictionary keyed by header field names with values.
header fields not provided in match must be wildcarded for the
entry to be considered matching.
Returns: a list of the flowmods that will be applied to the packet
represented by match
"""
packet_dict = match.copy() # Packet headers may be modified
instructions = []
table_id = 0
goto_table = True
while goto_table:
goto_table = False
table = self.tables[table_id]
matching_fte = None
# find a matching flowmod
for fte in table:
if fte.pkt_matches(packet_dict):
matching_fte = fte
break
# if a flowmod is found, make modifications to the match values and
# determine if another lookup is necessary
if trace:
sys.stderr.write("%d: %s\n" % (table_id, matching_fte))
if matching_fte:
for instruction in matching_fte.instructions:
instructions.append(instruction)
if instruction.type == ofp.OFPIT_GOTO_TABLE:
if table_id < instruction.table_id:
table_id = instruction.table_id
goto_table = True
elif instruction.type == ofp.OFPIT_APPLY_ACTIONS:
for action in instruction.actions:
if action.type == ofp.OFPAT_SET_FIELD:
packet_dict[action.key] = action.value
elif instruction.type == ofp.OFPIT_WRITE_METADATA:
metadata = packet_dict.get("metadata", 0)
mask = instruction.metadata_mask
mask_compl = mask ^ 0xFFFFFFFFFFFFFFFF
packet_dict["metadata"] = (metadata & mask_compl) | (
instruction.metadata & mask
)
return (instructions, packet_dict)
def flow_count(self):
"""Return number of flow tables rules"""
return sum(map(len, self.tables))
def is_output(self, match, port=None, vid=None, trace=False):
"""Return true if packets with match fields is output to port with
correct vlan.
If port is none it will return true if output to any port (including
special ports) regardless of vlan tag.
If vid is none it will return true if output to specified port
regardless of vlan tag.
To specify checking that the packet should not have a vlan tag, set the
OFPVID_PRESENT bit in vid to 0.
Arguments:
Match: a dictionary keyed by header field names with values.
"""
full_output = self.is_full_output(match.copy(), port, vid, trace)
def _output_result(action, vid_stack, port, vid):
if port is None:
return True
in_port = match.get("in_port")
result = None
if action.port == port:
if port == in_port:
result = None
elif vid is None:
result = True
elif vid & ofp.OFPVID_PRESENT == 0:
result = not vid_stack
else:
result = bool(vid_stack and vid == vid_stack[-1])
elif action.port == ofp.OFPP_IN_PORT and port == in_port:
result = True
return result
def _process_vid_stack(action, vid_stack):
if action.type == ofp.OFPAT_PUSH_VLAN:
vid_stack.append(ofp.OFPVID_PRESENT)
elif action.type == ofp.OFPAT_POP_VLAN:
vid_stack.pop()
elif action.type == ofp.OFPAT_SET_FIELD:
if action.key == "vlan_vid":
vid_stack[-1] = action.value
return vid_stack
if trace:
sys.stderr.write(
"tracing packet flow %s matching to port %s, vid %s\n"
% (match, port, vid)
)
# vid_stack represents the packet's vlan stack, innermost label listed
# first
match_vid = match.get("vlan_vid", 0)
vid_stack = []
if match_vid & ofp.OFPVID_PRESENT != 0:
vid_stack.append(match_vid)
instructions, _ = self.lookup(match, trace=trace)
for instruction in instructions:
if instruction.type != ofp.OFPIT_APPLY_ACTIONS:
continue
for action in instruction.actions:
vid_stack = _process_vid_stack(action, vid_stack)
if action.type == ofp.OFPAT_OUTPUT:
output_result = _output_result(action, vid_stack, port, vid)
if output_result is not None:
if output_result != full_output:
raise FakeOFTableException("Output functions do not match")
return output_result
elif action.type == ofp.OFPAT_GROUP:
if action.group_id not in self.groups:
raise FakeOFTableException(
"output group not in group table: %s" % action
)
buckets = self.groups[action.group_id].buckets
for bucket in buckets:
bucket_vid_stack = vid_stack
for bucket_action in bucket.actions:
bucket_vid_stack = _process_vid_stack(
bucket_action, bucket_vid_stack
)
if bucket_action.type == ofp.OFPAT_OUTPUT:
output_result = _output_result(
bucket_action, vid_stack, port, vid
)
if output_result is not None:
if output_result != full_output:
raise FakeOFTableException(
"Output functions do not match"
)
return output_result
if full_output is not False:
raise FakeOFTableException("Output functions do not match")
return False
def apply_instructions_to_packet(self, match):
"""
Send packet through the fake OF table pipeline
Args:
match (dict): A dict keyed by header fields with values, represents
a packet
Returns:
dict: Modified match dict, represents packet that has been through
the pipeline with values possibly altered
"""
_, packet_dict = self.lookup(match)
return packet_dict
def __str__(self):
string = ""
for table_id, table in enumerate(self.tables):
string += "\n----- Table %u -----\n" % (table_id)
string += "\n".join(sorted([str(flowmod) for flowmod in table]))
return string
def sort_tables(self):
"""Sort flows in tables by priority order."""
self.tables = [sorted(table, reverse=True) for table in self.tables]
class FlowMod:
"""Represents a flow modification message and its corresponding entry in
the flow table.
"""
MAC_MATCH_FIELDS = (
"eth_src",
"eth_dst",
"arp_sha",
"arp_tha",
"ipv6_nd_sll",
"ipv6_nd_tll",
)
IPV4_MATCH_FIELDS = ("ipv4_src", "ipv4_dst", "arp_spa", "arp_tpa")
IPV6_MATCH_FIELDS = ("ipv6_src", "ipv6_dst", "ipv6_nd_target")
HEX_FIELDS = "eth_type"
def __init__(self, flowmod):
"""flowmod is a ryu flow modification message object"""
self.priority = flowmod.priority
self.cookie = flowmod.cookie
self.instructions = flowmod.instructions
self.validate_instructions()
self.match_values = {}
self.match_masks = {}
self.out_port = None
# flowmod can be an OFPFlowMod or an OFPStats
if isinstance(flowmod, parser.OFPFlowMod):
if (
flowmod.command in (ofp.OFPFC_DELETE, ofp.OFPFC_DELETE_STRICT)
and flowmod.out_port != ofp.OFPP_ANY
):
self.out_port = flowmod.out_port
for key, val in flowmod.match.items():
if isinstance(val, tuple):
val, mask = val
else:
mask = -1
mask = self.match_to_bits(key, mask)
val = self.match_to_bits(key, val) & mask
self.match_values[key] = val
self.match_masks[key] = mask
def validate_instructions(self):
instruction_types = set()
for instruction in self.instructions:
if instruction.type in instruction_types:
raise FakeOFTableException(
"FlowMod with Multiple instructions of the "
"same type: {}".format(self.instructions)
)
instruction_types.add(instruction.type)
def out_port_matches(self, other):
"""returns True if other has an output action to this flowmods
output_port"""
if self.out_port is None or self.out_port == ofp.OFPP_ANY:
return True
for instruction in other.instructions:
if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
for action in instruction.actions:
if action.type == ofp.OFPAT_OUTPUT:
if action.port == self.out_port:
return True
return False
def pkt_matches(self, pkt_dict):
"""returns True if pkt_dict matches this flow table entry.
args:
pkt_dict - a dictionary keyed by flow table match fields with
values
if an element is included in the flow table entry match fields but not
in the pkt_dict that is assumed to indicate a failed match
"""
# TODO: add cookie and out_group
for key, val in self.match_values.items():
if key not in pkt_dict:
return False
val_bits = self.match_to_bits(key, pkt_dict[key])
if val_bits != (val & self.match_masks[key]):
return False
return True
def _matches_match(self, other):
return (
self.priority == other.priority
and self.match_values == other.match_values
and self.match_masks == other.match_masks
)
def fte_matches(self, other, strict=False):
"""returns True if the flow table entry other matches this flowmod.
used for finding existing flow table entries that match with this
flowmod.
args:
other - a flowmod object
strict (bool) - whether to use strict matching (as defined in
of1.3 specification section 6.4)
"""
if not self.out_port_matches(other):
return False
if strict:
return self._matches_match(other)
for key, val in self.match_values.items():
if key not in other.match_values:
return False
if other.match_values[key] & self.match_masks[key] != val:
return False
return True
def overlaps(self, other):
"""returns True if any packet can match both self and other."""
# This is different from the matches method as matches assumes an
# undefined field is a failed match. In this case an undefined field is
# potentially an overlap and therefore is considered success
if other.priority != self.priority:
return False
for key, val in self.match_values.items():
if key in other.match_values:
if val & other.match_masks[key] != other.match_values[key]:
return False