class TestSetDelta(TestCase): def setUp(self): self.set = set("abc") self.delta = SetDelta(self.set) def test_add(self): self.delta.add("c") self.delta.add("d") # Only "d" added, "c" was already present. self.assertEqual(self.delta.added_entries, set(["d"])) # Now apply, should mutate the set. self.assertEqual(self.set, set("abc")) self.delta.apply_and_reset() self.assertEqual(self.set, set("abcd")) self.assertEqual(self.delta.added_entries, set()) def test_remove(self): self.delta.remove("c") self.delta.remove("d") # Only "c" added, "d" was already missing. self.assertEqual(self.delta.removed_entries, set(["c"])) # Now apply, should mutate the set. self.assertEqual(self.set, set("abc")) self.delta.apply_and_reset() self.assertEqual(self.set, set("ab")) self.assertEqual(self.delta.removed_entries, set()) def test_add_and_remove(self): self.delta.add("c") # No-op, already present. self.delta.add("d") # Put in added set. self.delta.add("e") # Will remain in added set. self.delta.remove("c") # Recorded in remove set. self.delta.remove("d") # Cancels the pending add only. self.delta.remove("f") # No-op. self.assertEqual(self.delta.added_entries, set("e")) self.assertEqual(self.delta.removed_entries, set("c")) self.delta.apply_and_reset() self.assertEqual(self.set, set("abe"))
class IpsetActor(Actor): """ Actor managing a single ipset. Batches up updates to minimise the number of actual dataplane updates. """ def __init__(self, ipset, qualifier=None): """ :param Ipset ipset: Ipset object to wrap. :param str qualifier: Actor qualifier string for logging. """ super(IpsetActor, self).__init__(qualifier=qualifier) self._ipset = ipset # Members - which entries should be in the ipset. self.members = None # SetDelta, used to track a sequence of changes. self.changes = None self._force_reprogram = True self.stopped = False @property def ipset_name(self): """ The name of the primary ipset. Safe to access from another greenlet; only accesses immutable state. """ return self._ipset.set_name def owned_ipset_names(self): """ This method is safe to call from another greenlet; it only accesses immutable state. :return: set of name of ipsets that this Actor owns and manages. the sets may or may not be present. """ return set([self._ipset.set_name, self._ipset.temp_set_name]) @actor_message() def replace_members(self, members): """ Replace the members of this ipset with the supplied set. :param set[str]|list[str] members: The IP address strings. This method takes a copy of the contents. """ _log.info("Replacing members of ipset %s", self.name) self.members = set(members) self._force_reprogram = True # Force a full rewrite of the set. self.changes = SetDelta(self.members) # Any changes now obsolete. @actor_message() def add_members(self, new_members): _log.debug("Adding %s to tag ipset %s", new_members, self.name) assert self.members is not None, ( "add_members() called before init by replace_members()" ) for member in new_members: self.changes.add(member) @actor_message() def remove_members(self, removed_members): _log.debug("Removing %s from tag ipset %s", removed_members, self.name) assert self.members is not None, ( "remove_members() called before init by replace_members()" ) for member in removed_members: self.changes.remove(member) def _finish_msg_batch(self, batch, results): _log.debug("IpsetActor._finish_msg_batch() called") if not self.stopped: self._sync_to_ipset() def _sync_to_ipset(self): _log.debug("Syncing %s to kernel", self.name) if self.changes.resulting_size > self._ipset.max_elem: _log.error("ipset %s exceeds maximum size %s. ipset will not " "be updated until size drops below %s.", self.ipset_name, self._ipset.max_elem, self._ipset.max_elem) return if not self._force_reprogram: # Just an incremental update, try to apply it as a delta. if not self.changes.empty: _log.debug("Normal update, attempting to apply as a delta:" "added=%s, removed=%s", self.changes.added_entries, self.changes.removed_entries) try: self._ipset.apply_changes(self.changes.added_entries, self.changes.removed_entries) except FailedSystemCall as e: _log.error("Failed to update ipset %s, attempting to " "do a full rewrite RC=%s, err=%s", self.name, e.retcode, e.stderr) self._force_reprogram = True # Either we're now in sync or we're about to try rewriting the ipset # as a whole. Either way, apply the changes to the members set. self.changes.apply_and_reset() if self._force_reprogram: # Initial update or post-failure, completely replace the ipset's # contents with an atomic swap. _log.debug("Replacing content of ipset %s with %s", self, self.members) self._ipset.replace_members(self.members) self._force_reprogram = False _log.debug("Finished syncing %s to kernel", self.name)
class IpsetActor(Actor): """ Actor managing a single ipset. Batches up updates to minimise the number of actual dataplane updates. """ def __init__(self, ipset, qualifier=None): """ :param Ipset ipset: Ipset object to wrap. :param str qualifier: Actor qualifier string for logging. """ super(IpsetActor, self).__init__(qualifier=qualifier) self._ipset = ipset # Members - which entries should be in the ipset. self.members = None # SetDelta, used to track a sequence of changes. self.changes = None self._force_reprogram = True self.stopped = False @property def ipset_name(self): """ The name of the primary ipset. Safe to access from another greenlet; only accesses immutable state. """ return self._ipset.set_name def owned_ipset_names(self): """ This method is safe to call from another greenlet; it only accesses immutable state. :return: set of name of ipsets that this Actor owns and manages. the sets may or may not be present. """ return set([self._ipset.set_name, self._ipset.temp_set_name]) @actor_message() def replace_members(self, members): """ Replace the members of this ipset with the supplied set. :param set[str]|list[str] members: The IP address strings. This method takes a copy of the contents. """ _log.info("Replacing members of ipset %s", self.name) self.members = set(members) self._force_reprogram = True # Force a full rewrite of the set. self.changes = SetDelta(self.members) # Any changes now obsolete. @actor_message() def add_members(self, new_members): _log.debug("Adding %s to tag ipset %s", new_members, self.name) assert self.members is not None, ( "add_members() called before init by replace_members()") for member in new_members: self.changes.add(member) @actor_message() def remove_members(self, removed_members): _log.debug("Removing %s from tag ipset %s", removed_members, self.name) assert self.members is not None, ( "remove_members() called before init by replace_members()") for member in removed_members: self.changes.remove(member) def _finish_msg_batch(self, batch, results): _log.debug("IpsetActor._finish_msg_batch() called") if not self.stopped: self._sync_to_ipset() def _sync_to_ipset(self): _log.debug("Syncing %s to kernel", self.name) if self.changes.resulting_size > self._ipset.max_elem: _log.error( "ipset %s exceeds maximum size %s. ipset will not " "be updated until size drops below %s.", self.ipset_name, self._ipset.max_elem, self._ipset.max_elem) return if not self._force_reprogram: # Just an incremental update, try to apply it as a delta. if not self.changes.empty: _log.debug( "Normal update, attempting to apply as a delta:" "added=%s, removed=%s", self.changes.added_entries, self.changes.removed_entries) try: self._ipset.apply_changes(self.changes.added_entries, self.changes.removed_entries) except FailedSystemCall as e: _log.error( "Failed to update ipset %s, attempting to " "do a full rewrite RC=%s, err=%s", self.name, e.retcode, e.stderr) self._force_reprogram = True # Either we're now in sync or we're about to try rewriting the ipset # as a whole. Either way, apply the changes to the members set. self.changes.apply_and_reset() if self._force_reprogram: # Initial update or post-failure, completely replace the ipset's # contents with an atomic swap. _log.debug("Replacing content of ipset %s with %s", self, self.members) self._ipset.replace_members(self.members) self._force_reprogram = False _log.debug("Finished syncing %s to kernel", self.name)