示例#1
0
class TestRefHelper(TestReferenceManager):
    def setUp(self):
        super(TestRefHelper, self).setUp()
        self._rh = RefHelper(self._rm,
                             self._rm,
                             self._rm.ready_callback)

    def test_no_refs(self):
        # With no references, we're ready but haven't been notified
        self.assertFalse(self._rm._ready_called)
        self.assertTrue(self._rh.ready)

        # Discarding non-existent references is allowed
        self._rh.discard_ref("foo")

    def test_acquire_discard_1(self):
        # Acquire a reference to 'foo' - it won't be ready immediately
        self._rh.acquire_ref("foo")
        self.assertFalse(self._rm._ready_called)
        self.assertFalse(self._rh.ready)

        # Spin the actor framework - we become ready
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)
        self.assertTrue(self._rm._ready_called)
        self.assertTrue(self._rh.ready)
        self.assertEqual(next(self._rh.iteritems())[0], "foo")

        # Acquiring an already-acquired reference is idempotent
        self._rh.acquire_ref("foo")
        self.assertTrue(self._rh.ready)

        # Discard the reference
        self._rh.discard_ref("foo")
        _, obj = self.call_via_cb(self._rm.get_and_incref, "baz", async=True)
        self.assertTrue(self._rh.ready)

    def test_sync_acquire_discard(self):
        # Acquire a reference and discard it before it's become ready
        self._rh.acquire_ref("foo")
        self.assertFalse(self._rh.ready)

        self._rh.discard_ref("foo")
        self.assertTrue(self._rh.ready)

        # Spin the actor framework
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)

    def test_acquire_discard_2(self):
        # Acquire two references
        self._rh.acquire_ref("foo")
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)
        self._rh.acquire_ref("baz")
        self.assertFalse(self._rh.ready)
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar2", async=True)
        acq_ids = list(key for key, value in self._rh.iteritems())
        self.assertItemsEqual(acq_ids, ["foo", "baz"])
        self.assertTrue(self._rh.ready)

        # Discard them all!
        self._rh.discard_all()
示例#2
0
class TestRefHelper(TestReferenceManager):
    def setUp(self):
        super(TestRefHelper, self).setUp()
        self._rh = RefHelper(self._rm,
                             self._rm,
                             self._rm.ready_callback)

    def test_no_refs(self):
        # With no references, we're ready but haven't been notified
        self.assertFalse(self._rm._ready_called)
        self.assertTrue(self._rh.ready)

        # Discarding non-existent references is allowed
        self._rh.discard_ref("foo")

    def test_acquire_discard_1(self):
        # Acquire a reference to 'foo' - it won't be ready immediately
        self._rh.acquire_ref("foo")
        self.assertFalse(self._rm._ready_called)
        self.assertFalse(self._rh.ready)

        # Spin the actor framework - we become ready
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)
        self.assertTrue(self._rm._ready_called)
        self.assertTrue(self._rh.ready)
        self.assertEqual(next(self._rh.iteritems())[0], "foo")

        # Acquiring an already-acquired reference is idempotent
        self._rh.acquire_ref("foo")
        self.assertTrue(self._rh.ready)

        # Discard the reference
        self._rh.discard_ref("foo")
        _, obj = self.call_via_cb(self._rm.get_and_incref, "baz", async=True)
        self.assertTrue(self._rh.ready)

    def test_sync_acquire_discard(self):
        # Acquire a reference and discard it before it's become ready
        self._rh.acquire_ref("foo")
        self.assertFalse(self._rh.ready)

        self._rh.discard_ref("foo")
        self.assertTrue(self._rh.ready)

        # Spin the actor framework
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)

    def test_acquire_discard_2(self):
        # Acquire two references
        self._rh.acquire_ref("foo")
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar", async=True)
        self._rh.acquire_ref("baz")
        self.assertFalse(self._rh.ready)
        _, obj = self.call_via_cb(self._rm.get_and_incref, "bar2", async=True)
        acq_ids = list(key for key, value in self._rh.iteritems())
        self.assertItemsEqual(acq_ids, ["foo", "baz"])
        self.assertTrue(self._rh.ready)

        # Discard them all!
        self._rh.discard_all()
示例#3
0
class ProfileRules(RefCountedActor):
    """
    Actor that owns the per-profile rules chains.
    """
    def __init__(self, profile_id, ip_version, iptables_updater, ipset_mgr):
        super(ProfileRules, self).__init__(qualifier=profile_id)
        assert profile_id is not None

        self.id = profile_id
        self.ip_version = ip_version
        self._ipset_mgr = ipset_mgr
        self._iptables_updater = iptables_updater
        self._ipset_refs = RefHelper(self, ipset_mgr, self._on_ipsets_acquired)

        # Latest profile update - a profile dictionary.
        self._pending_profile = None
        # Currently-programmed profile dictionary.
        self._profile = None

        # State flags.
        self._notified_ready = False
        self._cleaned_up = False
        self._dead = False
        self._dirty = True

        self.chain_names = {
            "inbound": profile_to_chain_name("inbound", profile_id),
            "outbound": profile_to_chain_name("outbound", profile_id),
        }
        _log.info("Profile %s has chain names %s", profile_id,
                  self.chain_names)

    @actor_message()
    def on_profile_update(self, profile, force_reprogram=False):
        """
        Update the programmed iptables configuration with the new
        profile.

        :param dict[str]|NoneType profile: Dictionary of all profile data or
            None if profile is to be deleted.
        """
        _log.debug("%s: Profile update: %s", self, profile)
        assert not self._dead, "Shouldn't receive updates after we're dead."
        self._pending_profile = profile
        self._dirty |= force_reprogram

    @actor_message()
    def on_unreferenced(self):
        """
        Called to tell us that this profile is no longer needed.
        """
        # Flag that we're dead and then let finish_msg_batch() do the cleanup.
        self._dead = True

    def _on_ipsets_acquired(self):
        """
        Callback from the RefHelper once it's acquired all the ipsets we
        need.

        This is called from an actor_message on our greenlet.
        """
        # Nothing to do here, if this is being called then we're already in
        # a message batch so _finish_msg_batch() will get called next.
        _log.info("All required ipsets acquired.")

    def _finish_msg_batch(self, batch, results):
        # Due to dependency management in IptablesUpdater, we don't need to
        # worry about programming the dataplane before notifying so do it on
        # this common code path.
        if not self._notified_ready:
            self._notify_ready()
            self._notified_ready = True

        if self._dead:
            # Only want to clean up once.  Note: we can get here a second time
            # if we had a pending ipset incref in-flight when we were asked
            # to clean up.
            if not self._cleaned_up:
                try:
                    _log.info("%s unreferenced, removing our chains", self)
                    self._delete_chains()
                    self._ipset_refs.discard_all()
                    self._ipset_refs = None  # Break ref cycle.
                    self._profile = None
                    self._pending_profile = None
                finally:
                    self._cleaned_up = True
                    self._notify_cleanup_complete()
        else:
            if self._pending_profile != self._profile:
                _log.debug("Profile data changed, updating ipset references.")
                old_tags = extract_tags_from_profile(self._profile)
                new_tags = extract_tags_from_profile(self._pending_profile)
                removed_tags = old_tags - new_tags
                added_tags = new_tags - old_tags
                for tag in removed_tags:
                    _log.debug("Queueing ipset for tag %s for decref", tag)
                    self._ipset_refs.discard_ref(tag)
                for tag in added_tags:
                    _log.debug("Requesting ipset for tag %s", tag)
                    self._ipset_refs.acquire_ref(tag)
                self._dirty = True
                self._profile = self._pending_profile

            if (self._dirty and self._ipset_refs.ready
                    and self._pending_profile is not None):
                _log.info("Ready to program rules for %s", self.id)
                try:
                    self._update_chains()
                except FailedSystemCall as e:
                    _log.error("Failed to program profile chain %s; error: %r",
                               self, e)
                else:
                    self._dirty = False
            elif not self._dirty:
                _log.debug("No changes to program.")
            elif self._pending_profile is None:
                _log.info("Profile is None, removing our chains")
                try:
                    self._delete_chains()
                except FailedSystemCall:
                    _log.exception("Failed to delete chains for profile %s",
                                   self.id)
                else:
                    self._dirty = False
            elif not self._ipset_refs.ready:
                _log.info("Can't program rules %s yet, waiting on ipsets",
                          self.id)

    def _delete_chains(self):
        """
        Removes our chains from the dataplane, blocks until complete.
        """
        chains = set(self.chain_names.values())
        # Need to block here: have to wait for chains to be deleted
        # before we can decref our ipsets.
        self._iptables_updater.delete_chains(chains, async=False)

    def _update_chains(self):
        """
        Updates the chains in the dataplane.

        Blocks until the update is complete.

        On entry, self._pending_profile must not be None.

        :raises FailedSystemCall: if the update fails.
        """
        _log.info("%s Programming iptables with our chains.", self)
        assert self._pending_profile is not None, \
               "_update_chains called with no _pending_profile"
        updates = {}
        for direction in ("inbound", "outbound"):
            chain_name = self.chain_names[direction]
            _log.info("Updating %s chain %r for profile %s", direction,
                      chain_name, self.id)
            _log.debug("Profile %s: %s", self.id, self._profile)
            rules_key = "%s_rules" % direction
            new_rules = self._pending_profile.get(rules_key, [])
            tag_to_ip_set_name = {}
            for tag, ipset in self._ipset_refs.iteritems():
                tag_to_ip_set_name[tag] = ipset.ipset_name
            updates[chain_name] = rules_to_chain_rewrite_lines(
                chain_name,
                new_rules,
                self.ip_version,
                tag_to_ip_set_name,
                on_allow="RETURN",
                comment_tag=self.id)
        _log.debug("Queueing programming for rules %s: %s", self.id, updates)
        self._iptables_updater.rewrite_chains(updates, {}, async=False)
示例#4
0
class ProfileRules(RefCountedActor):
    """
    Actor that owns the per-profile rules chains.
    """
    def __init__(self, profile_id, ip_version, iptables_updater, ipset_mgr):
        super(ProfileRules, self).__init__(qualifier=profile_id)
        assert profile_id is not None

        self.id = profile_id
        self.ip_version = ip_version
        self.ipset_mgr = ipset_mgr
        self._iptables_updater = iptables_updater
        self.notified_ready = False

        self.ipset_refs = RefHelper(self, ipset_mgr, self._maybe_update)

        self._profile = None
        """
        :type dict|None: filled in by first update.  Reset to None on delete.
        """
        self.dead = False

        self.chain_names = {
            "inbound": profile_to_chain_name("inbound", profile_id),
            "outbound": profile_to_chain_name("outbound", profile_id),
        }
        _log.info("Profile %s has chain names %s",
                  profile_id, self.chain_names)

    @actor_message()
    def on_profile_update(self, profile):
        """
        Update the programmed iptables configuration with the new
        profile.
        """
        _log.debug("%s: Profile update: %s", self, profile)
        assert profile is None or profile["id"] == self.id
        assert not self.dead, "Shouldn't receive updates after we're dead."

        old_tags = extract_tags_from_profile(self._profile)
        new_tags = extract_tags_from_profile(profile)

        removed_tags = old_tags - new_tags
        added_tags = new_tags - old_tags
        for tag in removed_tags:
            _log.debug("Queueing ipset for tag %s for decref", tag)
            self.ipset_refs.discard_ref(tag)
        for tag in added_tags:
            _log.debug("Requesting ipset for tag %s", tag)
            self.ipset_refs.acquire_ref(tag)

        self._profile = profile
        self._maybe_update()

    def _maybe_update(self):
        if self.dead:
            _log.info("Not updating: profile is dead.")
        elif not self.ipset_refs.ready:
            _log.info("Can't program rules %s yet, waiting on ipsets",
                      self.id)
        else:
            _log.info("Ready to program rules for %s", self.id)
            self._update_chains()

    @actor_message()
    def on_unreferenced(self):
        """
        Called to tell us that this profile is no longer needed.  Removes
        our iptables configuration.
        """
        try:
            _log.info("%s unreferenced, removing our chains", self)
            self.dead = True
            chains = []
            for direction in ["inbound", "outbound"]:
                chain_name = self.chain_names[direction]
                chains.append(chain_name)
            self._iptables_updater.delete_chains(chains, async=False)
            self.ipset_refs.discard_all()
            self.ipset_refs = None # Break ref cycle.
            self._profile = None
        finally:
            self._notify_cleanup_complete()

    def _update_chains(self):
        """
        Updates the chains in the dataplane.
        """
        _log.info("%s Programming iptables with our chains.", self)
        updates = {}
        for direction in ("inbound", "outbound"):
            _log.debug("Updating %s chain for profile %s", direction,
                       self.id)
            new_profile = self._profile or {}
            _log.debug("Profile %s: %s", self.id, self._profile)
            rules_key = "%s_rules" % direction
            new_rules = new_profile.get(rules_key, [])
            chain_name = self.chain_names[direction]
            tag_to_ip_set_name = {}
            for tag, ipset in self.ipset_refs.iteritems():
                tag_to_ip_set_name[tag] = ipset.name
            updates[chain_name] = rules_to_chain_rewrite_lines(
                chain_name,
                new_rules,
                self.ip_version,
                tag_to_ip_set_name,
                on_allow="RETURN")
        _log.debug("Queueing programming for rules %s: %s", self.id,
                   updates)
        self._iptables_updater.rewrite_chains(updates, {}, async=False)
        # TODO Isolate exceptions from programming the chains to this profile.
        # Radical thought - could we just say that the profile should be OK,
        # and therefore we don't care? In other words, do we need to handle the
        # error cleverly, or could we just say that since we built the rules
        # they really should always work.
        if not self.notified_ready:
            self._notify_ready()
            self.notified_ready = True
class ProfileRules(RefCountedActor):
    """
    Actor that owns the per-profile rules chains.
    """
    def __init__(self, profile_id, ip_version, iptables_updater, ipset_mgr):
        super(ProfileRules, self).__init__(qualifier=profile_id)
        assert profile_id is not None

        self.id = profile_id
        self.ip_version = ip_version
        self._ipset_mgr = ipset_mgr
        self._iptables_updater = iptables_updater
        self._ipset_refs = RefHelper(self, ipset_mgr, self._on_ipsets_acquired)

        # Latest profile update - a profile dictionary.
        self._pending_profile = None
        # Currently-programmed profile dictionary.
        self._profile = None

        # State flags.
        self._notified_ready = False
        self._cleaned_up = False
        self._dead = False
        self._dirty = True

        self.chain_names = {
            "inbound": profile_to_chain_name("inbound", profile_id),
            "outbound": profile_to_chain_name("outbound", profile_id),
        }
        _log.info("Profile %s has chain names %s",
                  profile_id, self.chain_names)

    @actor_message()
    def on_profile_update(self, profile, force_reprogram=False):
        """
        Update the programmed iptables configuration with the new
        profile.

        :param dict[str]|NoneType profile: Dictionary of all profile data or
            None if profile is to be deleted.
        """
        _log.debug("%s: Profile update: %s", self, profile)
        assert not self._dead, "Shouldn't receive updates after we're dead."
        self._pending_profile = profile
        self._dirty |= force_reprogram

    @actor_message()
    def on_unreferenced(self):
        """
        Called to tell us that this profile is no longer needed.
        """
        # Flag that we're dead and then let finish_msg_batch() do the cleanup.
        self._dead = True

    def _on_ipsets_acquired(self):
        """
        Callback from the RefHelper once it's acquired all the ipsets we
        need.

        This is called from an actor_message on our greenlet.
        """
        # Nothing to do here, if this is being called then we're already in
        # a message batch so _finish_msg_batch() will get called next.
        _log.info("All required ipsets acquired.")

    def _finish_msg_batch(self, batch, results):
        # Due to dependency management in IptablesUpdater, we don't need to
        # worry about programming the dataplane before notifying so do it on
        # this common code path.
        if not self._notified_ready:
            self._notify_ready()
            self._notified_ready = True

        if self._dead:
            # Only want to clean up once.  Note: we can get here a second time
            # if we had a pending ipset incref in-flight when we were asked
            # to clean up.
            if not self._cleaned_up:
                try:
                    _log.info("%s unreferenced, removing our chains", self)
                    self._delete_chains()
                    self._ipset_refs.discard_all()
                    self._ipset_refs = None # Break ref cycle.
                    self._profile = None
                    self._pending_profile = None
                finally:
                    self._cleaned_up = True
                    self._notify_cleanup_complete()
        else:
            if self._pending_profile != self._profile:
                _log.debug("Profile data changed, updating ipset references.")
                old_tags = extract_tags_from_profile(self._profile)
                new_tags = extract_tags_from_profile(self._pending_profile)
                removed_tags = old_tags - new_tags
                added_tags = new_tags - old_tags
                for tag in removed_tags:
                    _log.debug("Queueing ipset for tag %s for decref", tag)
                    self._ipset_refs.discard_ref(tag)
                for tag in added_tags:
                    _log.debug("Requesting ipset for tag %s", tag)
                    self._ipset_refs.acquire_ref(tag)
                self._dirty = True
                self._profile = self._pending_profile

            if (self._dirty and
                    self._ipset_refs.ready and
                    self._pending_profile is not None):
                _log.info("Ready to program rules for %s", self.id)
                try:
                    self._update_chains()
                except FailedSystemCall as e:
                    _log.error("Failed to program profile chain %s; error: %r",
                               self, e)
                else:
                    self._dirty = False
            elif not self._dirty:
                _log.debug("No changes to program.")
            elif self._pending_profile is None:
                _log.info("Profile is None, removing our chains")
                try:
                    self._delete_chains()
                except FailedSystemCall:
                    _log.exception("Failed to delete chains for profile %s",
                                   self.id)
                else:
                    self._dirty = False
            elif not self._ipset_refs.ready:
                _log.info("Can't program rules %s yet, waiting on ipsets",
                          self.id)

    def _delete_chains(self):
        """
        Removes our chains from the dataplane, blocks until complete.
        """
        chains = set(self.chain_names.values())
        # Need to block here: have to wait for chains to be deleted
        # before we can decref our ipsets.
        self._iptables_updater.delete_chains(chains, async=False)

    def _update_chains(self):
        """
        Updates the chains in the dataplane.

        Blocks until the update is complete.

        On entry, self._pending_profile must not be None.

        :raises FailedSystemCall: if the update fails.
        """
        _log.info("%s Programming iptables with our chains.", self)
        assert self._pending_profile is not None, \
               "_update_chains called with no _pending_profile"
        updates = {}
        for direction in ("inbound", "outbound"):
            chain_name = self.chain_names[direction]
            _log.info("Updating %s chain %r for profile %s",
                      direction, chain_name, self.id)
            _log.debug("Profile %s: %s", self.id, self._profile)
            rules_key = "%s_rules" % direction
            new_rules = self._pending_profile.get(rules_key, [])
            tag_to_ip_set_name = {}
            for tag, ipset in self._ipset_refs.iteritems():
                tag_to_ip_set_name[tag] = ipset.ipset_name
            updates[chain_name] = rules_to_chain_rewrite_lines(
                chain_name,
                new_rules,
                self.ip_version,
                tag_to_ip_set_name,
                on_allow="RETURN",
                comment_tag=self.id)
        _log.debug("Queueing programming for rules %s: %s", self.id,
                   updates)
        self._iptables_updater.rewrite_chains(updates, {}, async=False)
示例#6
0
class ProfileRules(RefCountedActor):
    """
    Actor that owns the per-profile rules chains.
    """
    def __init__(self, profile_id, ip_version, iptables_updater, ipset_mgr):
        super(ProfileRules, self).__init__(qualifier=profile_id)
        assert profile_id is not None

        self.id = profile_id
        self.ip_version = ip_version
        self.ipset_mgr = ipset_mgr
        self._iptables_updater = iptables_updater
        self.notified_ready = False

        self.ipset_refs = RefHelper(self, ipset_mgr, self._maybe_update)

        self._profile = None
        """
        :type dict|None: filled in by first update.  Reset to None on delete.
        """
        self.dead = False

        self.chain_names = {
            "inbound": profile_to_chain_name("inbound", profile_id),
            "outbound": profile_to_chain_name("outbound", profile_id),
        }
        _log.info("Profile %s has chain names %s",
                  profile_id, self.chain_names)

    @actor_message()
    def on_profile_update(self, profile):
        """
        Update the programmed iptables configuration with the new
        profile.
        """
        _log.debug("%s: Profile update: %s", self, profile)
        assert profile is None or profile["id"] == self.id
        assert not self.dead, "Shouldn't receive updates after we're dead."

        old_tags = extract_tags_from_profile(self._profile)
        new_tags = extract_tags_from_profile(profile)

        removed_tags = old_tags - new_tags
        added_tags = new_tags - old_tags
        for tag in removed_tags:
            _log.debug("Queueing ipset for tag %s for decref", tag)
            self.ipset_refs.discard_ref(tag)
        for tag in added_tags:
            _log.debug("Requesting ipset for tag %s", tag)
            self.ipset_refs.acquire_ref(tag)

        self._profile = profile
        self._maybe_update()

    def _maybe_update(self):
        if self.dead:
            _log.info("Not updating: profile is dead.")
        elif not self.ipset_refs.ready:
            _log.info("Can't program rules %s yet, waiting on ipsets",
                      self.id)
        else:
            _log.info("Ready to program rules for %s", self.id)
            self._update_chains()

    @actor_message()
    def on_unreferenced(self):
        """
        Called to tell us that this profile is no longer needed.  Removes
        our iptables configuration.
        """
        try:
            _log.info("%s unreferenced, removing our chains", self)
            self.dead = True
            chains = []
            for direction in ["inbound", "outbound"]:
                chain_name = self.chain_names[direction]
                chains.append(chain_name)
            self._iptables_updater.delete_chains(chains, async=False)
            self.ipset_refs.discard_all()
            self.ipset_refs = None # Break ref cycle.
            self._profile = None
        finally:
            self._notify_cleanup_complete()

    def _update_chains(self):
        """
        Updates the chains in the dataplane.
        """
        _log.info("%s Programming iptables with our chains: %s")
        updates = {}
        for direction in ("inbound", "outbound"):
            _log.debug("Updating %s chain for profile %s", direction,
                       self.id)
            new_profile = self._profile or {}
            _log.debug("Profile %s: %s", self.id, self._profile)
            rules_key = "%s_rules" % direction
            new_rules = new_profile.get(rules_key, [])
            chain_name = self.chain_names[direction]
            tag_to_ip_set_name = {}
            for tag, ipset in self.ipset_refs.iteritems():
                tag_to_ip_set_name[tag] = ipset.name
            updates[chain_name] = rules_to_chain_rewrite_lines(
                chain_name,
                new_rules,
                self.ip_version,
                tag_to_ip_set_name,
                on_allow="RETURN")
        _log.debug("Queueing programming for rules %s: %s", self.id,
                   updates)
        self._iptables_updater.rewrite_chains(updates, {}, async=False)
        # TODO Isolate exceptions from programming the chains to this profile.
        # PLW: Radical thought - could we just say that the profile should be
        # OK, and therefore we don't care? In other words, do we need to handle
        # the error cleverly in the short term, or could we just say that since
        # we built the rules they really should always work.
        if not self.notified_ready:
            self._notify_ready()
            self.notified_ready = True