blob: 37afdc3f5c47560266852b9955e08fcb92be2cbf [file] [log] [blame]
Zack Williams41513bf2018-07-07 20:08:35 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Nathan Knuth418fdc82016-09-16 22:51:15 -070014"""
15Internal state of the device
16"""
17
18import logging
Zsolt Haraszti023ea7c2016-10-16 19:30:34 -070019from ofagent.utils import pp
Nathan Knuth418fdc82016-09-16 22:51:15 -070020import loxi.of13 as ofp
21
22
23class GroupEntry(object):
24 def __init__(self, group_desc, group_stats):
25 assert isinstance(group_desc, ofp.group_desc_stats_entry)
26 assert isinstance(group_stats, ofp.group_stats_entry)
27 self.group_desc = group_desc
28 self.group_stats = group_stats
29
30
31def flow_stats_entry_from_flow_mod_message(fmm):
32 assert isinstance(fmm, ofp.message.flow_mod)
33
34 # extract a flow stats entry from a flow_mod message
35 kw = fmm.__dict__
36
37 # drop these from the object
38 for k in ('xid', 'cookie_mask', 'out_port', 'buffer_id', 'out_group'):
39 del kw[k]
40 flow = ofp.flow_stats_entry(
41 duration_sec=0,
42 duration_nsec=0,
43 packet_count=0,
44 byte_count=0,
45 **kw
46 )
47 return flow
48
49def group_entry_from_group_mod_message(gmm):
50 assert isinstance(gmm, ofp.message.group_mod)
51
52 kw = gmm.__dict__
53
54 # drop these attributes from the object:
55 for k in ('xid',):
56 del kw[k]
57
58 group_desc = ofp.group_desc_stats_entry(
59 group_type=gmm.group_type,
60 group_id=gmm.group_id,
61 buckets=gmm.buckets
62 )
63
64 group_stats = ofp.group_stats_entry(
65 group_id=gmm.group_id
66 )
67
68 return GroupEntry(group_desc, group_stats)
69
70
71class ObjectStore(object):
72
73 def __init__(self):
74 self.ports = [] # list of ofp.common.port_desc
75 self.flows = [] # list of ofp.common.flow_stats_entry
76 self.groups = {} # dict of (ofp.group_desc_stats_entry, ofp.group_stats_entry) tuples,
77 # keyed by the group_id field
78 self.agent = None
79
80 def set_agent(self, agent):
81 """Set agent reference"""
82 self.agent = agent
83
84 def signal_flow_mod_error(self, code, flow_mod):
85 """Forward error to agent"""
86 if self.agent is not None:
87 agent.signal_flow_mod_error(code, flow_mod)
88
89 def signal_flow_removal(self, flow):
90 """Forward flow removal notification to agent"""
91 if self.agent is not None:
92 agent.signal_flow_removal(flow)
93
94 def signal_group_mod_error(self, code, group_mod):
95 if self.agent is not None:
96 agent.signal_group_mod_error(code, group_mod)
97
98 ## <=========================== PORT HANDLERS ==================================>
99
100 def port_list(self):
101 return self.ports
102
103 def port_add(self, port):
104 self.ports.append(port)
105
106 def port_stats(self):
107 logging.warn("port_stats() not yet implemented")
108 return []
109
110 ## <=========================== FLOW HANDLERS ==================================>
111
112 def flow_add(self, flow_add):
113 assert isinstance(flow_add, ofp.message.flow_add)
114 assert flow_add.cookie_mask == 0
115
116 check_overlap = flow_add.flags & ofp.OFPFF_CHECK_OVERLAP
117 if check_overlap:
Zsolt Haraszti023ea7c2016-10-16 19:30:34 -0700118 if self._flow_find_overlapping_flows(flow_add, return_on_first_hit=True):
Nathan Knuth418fdc82016-09-16 22:51:15 -0700119 self.signal_flow_mod_error(ofp.OFPFMFC_OVERLAP, flow_add)
120 else:
121 # free to add as new flow
122 flow = flow_stats_entry_from_flow_mod_message(flow_add)
123 self.flows.append(flow)
124
125 else:
126 flow = flow_stats_entry_from_flow_mod_message(flow_add)
127 idx = self._flow_find(flow)
128 if idx >= 0:
129 old_flow = self.flows[idx]
130 assert isinstance(old_flow, ofp.common.flow_stats_entry)
131 if not (flow_add.flags & ofp.OFPFF_RESET_COUNTS):
132 flow.byte_count = old_flow.byte_count
133 flow.packet_count = old_flow.packet_count
134 self.flows[idx] = flow
135
136 else:
137 self.flows.append(flow)
138
139 def flow_delete_strict(self, flow_delete_strict):
140 assert isinstance(flow_delete_strict, ofp.message.flow_delete_strict)
141 flow = flow_stats_entry_from_flow_mod_message(flow_delete_strict)
142 idx = self._flow_find(flow)
143 if (idx >= 0):
144 del self.flows[idx]
145 logging.info("flow removed:\n%s" % pp(flow))
146 else:
147 logging.error('Requesting strict delete of:\n%s\nwhen flow table is:\n\n' % pp(flow))
148 for f in self.flows:
149 print pp(f)
150
151 def flow_delete(self, flow_delete):
152 assert isinstance(flow_delete, ofp.message.flow_delete)
153
154 # build a list of what to keep vs what to delete
155 to_keep = []
156 to_delete = []
157 for f in self.flows:
158 list_to_append = to_delete if self._flow_matches_spec(f, flow_delete) else to_keep
159 list_to_append.append(f)
160
161 # replace flow table with keepers
162 self.flows = to_keep
163
164 # send notifications for discarded flows as required by OF spec
165 self._announce_flows_deleted(to_delete)
166
167 def flow_modify_strict(self, flow_obj):
168 raise Exception("flow_modify_strict(): Not implemented yet")
169
170 def flow_modify(self, flow_obj):
171 raise Exception("flow_modify(): Not implemented yet")
172
173 def flow_list(self):
174 return self.flows
175
176 def _flow_find_overlapping_flows(self, flow_mod, return_on_first_hit=False):
177 """
178 Return list of overlapping flow(s)
179 Two flows overlap if a packet may match both and if they have the same priority.
180 """
181
182 def _flow_find(self, flow):
183 for i, f in enumerate(self.flows):
184 if self._flows_match(f, flow):
185 return i
186 return -1
187
188 def _flows_match(self, f1, f2):
189 keys_matter = ('table_id', 'priority', 'flags', 'cookie', 'match')
190 for key in keys_matter:
191 if getattr(f1, key) != getattr(f2, key):
192 return False
193 return True
194
195 def _flow_matches_spec(self, flow, flow_mod):
196 """
197 Return True if a given flow (flow_stats_entry) is "covered" by the wildcarded flow
198 mod or delete spec (as defined in the flow_mod or flow_delete message); otherwise
199 return False.
200 """
201 #import pdb
202 #pdb.set_trace()
203
204 assert isinstance(flow, ofp.common.flow_stats_entry)
205 assert isinstance(flow_mod, (ofp.message.flow_delete, ofp.message.flow_mod))
206
207 # Check if flow.cookie is a match for flow_mod cookie/cookie_mask
208 if (flow.cookie & flow_mod.cookie_mask) != (flow_mod.cookie & flow_mod.cookie_mask):
209 return False
210
211 # Check if flow.table_id is covered by flow_mod.table_id
212 if flow_mod.table_id != ofp.OFPTT_ALL and flow.table_id != flow_mod.table_id:
213 return False
214
215 # Check out_port
216 if flow_mod.out_port != ofp.OFPP_ANY and not self._flow_has_out_port(flow, flow_mod.out_port):
217 return False
218
219 # Check out_group
220 if flow_mod.out_group != ofp.OFPG_ANY and not self._flow_has_out_group(flow, flow_mod.out_group):
221 return False
222
223 # Priority is ignored
224
225 # Check match condition
226 # If the flow_mod match field is empty, that is a special case and indicate the flow entry matches
227 match = flow_mod.match
228 assert isinstance(match, ofp.common.match_v3)
229 if not match.oxm_list:
230 return True # If we got this far and the match is empty in the flow spec, than the flow matches
231 else:
232 raise NotImplementedError("_flow_matches_spec(): No flow match analysys yet")
233
234 def _flow_has_out_port(self, flow, out_port):
235 """Return True if flow has a output command with the given out_port"""
236 assert isinstance(flow, ofp.common.flow_stats_entry)
237 for instruction in flow.instructions:
238 assert isinstance(instruction, ofp.instruction.instruction)
239 if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
240 assert isinstance(instruction, ofp.instruction.apply_actions)
241 for action in instruction.actions:
242 assert isinstance(action, ofp.action.action)
243 if action.type == ofp.OFPAT_OUTPUT:
244 assert isinstance(action, ofp.action.output)
245 if action.port == out_port:
246 return True
247
248 # otherwise...
249 return False
250
251 def _flow_has_out_group(self, flow, out_group):
252 """Return True if flow has a output command with the given out_group"""
253 assert isinstance(flow, ofp.common.flow_stats_entry)
254 for instruction in flow.instructions:
255 assert isinstance(instruction, ofp.instruction.instruction)
256 if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
257 assert isinstance(instruction, ofp.instruction.apply_actions)
258 for action in instruction.actions:
259 assert isinstance(action, ofp.action.action)
260 if action.type == ofp.OFPAT_GROUP:
261 assert isinstance(action, ofp.action.group)
262 if action.group_id == out_group:
263 return True
264
265 # otherwise...
266 return False
267
268 def _flows_delete_by_group_id(self, group_id):
269 """Delete any flow referring to given group id"""
270 to_keep = []
271 to_delete = []
272 for f in self.flows:
273 list_to_append = to_delete if self._flow_has_out_group(f, group_id) else to_keep
274 list_to_append.append(f)
275
276 # replace flow table with keepers
277 self.flows = to_keep
278
279 # send notifications for discarded flows as required by OF spec
280 self._announce_flows_deleted(to_delete)
281
282 def _announce_flows_deleted(self, flows):
283 for f in flows:
284 self._announce_flow_deleted(f)
285
286 def _announce_flow_deleted(self, flow):
287 """Send notification to controller if flow is flagged with OFPFF_SEND_FLOW_REM flag"""
288 if flow.flags & ofp.OFPFF_SEND_FLOW_REM:
289 raise NotImplementedError("_announce_flow_deleted()")
290
291
292 ## <=========================== GROUP HANDLERS =================================>
293
294 def group_add(self, group_add):
295 assert isinstance(group_add, ofp.message.group_add)
296 if group_add.group_id in self.groups:
297 self.signal_group_mod_error(ofp.OFPGMFC_GROUP_EXISTS, group_add)
298 else:
299 group_entry = group_entry_from_group_mod_message(group_add)
300 self.groups[group_add.group_id] = group_entry
301
302 def group_modify(self, group_modify):
303 assert isinstance(group_modify, ofp.message.group_modify)
304 if group_modify.group_id not in self.groups:
305 self.signal_group_mod_error(ofp.OFPGMFC_INVALID_GROUP, group_modify)
306 else:
Zsolt Haraszti023ea7c2016-10-16 19:30:34 -0700307 # replace existing group entry with new group definition
Nathan Knuth418fdc82016-09-16 22:51:15 -0700308 group_entry = group_entry_from_group_mod_message(group_modify)
309 self.groups[group_modify.group_id] = group_entry
310
311 def group_delete(self, group_delete):
312 assert isinstance(group_delete, ofp.message.group_mod)
313 if group_delete.group_id == ofp.OFPG_ALL: # drop all groups
314 # we must delete all flows that point to this group and signal controller as
315 # requested by the flows' flag
316
317 self.groups = {}
318 logging.info("all groups deleted")
319
320 else:
321 if group_delete.group_id not in self.groups:
322 # per the spec, this is silent;y ignored
323 return
324
325 else:
326 self._flows_delete_by_group_id(group_delete.group_id)
327 del self.groups[group_delete.group_id]
328 logging.info("group %d deleted" % group_delete.group_id)
329
330 def group_list(self):
331 return [group_entry.group_desc for group_entry in self.groups.values()]
332
333 def group_stats(self):
334 return [group_entry.group_stats for group_entry in self.groups.values()]
335
336 ## <=========================== TABLE HANDLERS =================================>
337
338 def table_stats(self):
339 """Scan through flow entries and create table stats"""
340 stats = {}
341 for flow in self.flows:
342 table_id = flow.table_id
343 entry = stats.setdefault(table_id, ofp.common.table_stats_entry(table_id))
344 entry.active_count += 1
345 entry.lookup_count += 1 # FIXME how to count these?
346 entry.matched_count += 1 # FIXME how to count these?
347 stats[table_id] = entry
348 return sorted(stats.values(), key=lambda e: e.table_id)