Nathan Knuth | 418fdc8 | 2016-09-16 22:51:15 -0700 | [diff] [blame] | 1 | """ |
| 2 | Internal state of the device |
| 3 | """ |
| 4 | |
| 5 | import logging |
Zsolt Haraszti | 023ea7c | 2016-10-16 19:30:34 -0700 | [diff] [blame] | 6 | from ofagent.utils import pp |
Nathan Knuth | 418fdc8 | 2016-09-16 22:51:15 -0700 | [diff] [blame] | 7 | import loxi.of13 as ofp |
| 8 | |
| 9 | |
| 10 | class 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 | |
| 18 | def 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 | |
| 36 | def 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 | |
| 58 | class 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 Haraszti | 023ea7c | 2016-10-16 19:30:34 -0700 | [diff] [blame] | 105 | if self._flow_find_overlapping_flows(flow_add, return_on_first_hit=True): |
Nathan Knuth | 418fdc8 | 2016-09-16 22:51:15 -0700 | [diff] [blame] | 106 | 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 Haraszti | 023ea7c | 2016-10-16 19:30:34 -0700 | [diff] [blame] | 294 | # replace existing group entry with new group definition |
Nathan Knuth | 418fdc8 | 2016-09-16 22:51:15 -0700 | [diff] [blame] | 295 | 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) |