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