Beispiel #1
0
 def __init__(self, label_index):
     self.label_index = label_index
     self.labels_by_item_id = {}
     self.labels_by_parent_id = {}
     self.parent_ids_by_item_id = {}
     self.item_ids_by_parent_id = MultiDict()
     self._dirty_items = set()
Beispiel #2
0
 def __init__(self):
     # Cache of raw label dicts by item_id.
     self.labels_by_item_id = {}
     # All expressions by ID.
     self.expressions_by_id = {}
     # Map from expression ID to matching item_ids.
     self.matches_by_expr_id = MultiDict()
     self.matches_by_item_id = MultiDict()
Beispiel #3
0
 def __init__(self):
     # Cache of raw label dicts by item_id.
     self.labels_by_item_id = {}
     # All expressions by ID.
     self.expressions_by_id = {}
     # Map from expression ID to matching item_ids.
     self.matches_by_expr_id = MultiDict()
     self.matches_by_item_id = MultiDict()
Beispiel #4
0
    def __init__(self, config, ip_type, iptables_updater, workload_disp_chains,
                 host_disp_chains, rules_manager, fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.workload_disp_chains = workload_disp_chains
        self.host_disp_chains = host_disp_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Cache of IPs applied to host endpoints.  (I.e. any interfaces that
        # aren't workload interfaces.)
        self.host_ep_ips_by_iface = {}
        # Host interface dicts by ID.  We'll resolve these with the IPs above
        # and inject the (resolved) ones as endpoints.
        self.host_eps_by_id = {}
        # Cache of interfaces that we've resolved and injected as endpoints.
        self.resolved_host_eps = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
        self._iface_poll_greenlet = gevent.Greenlet(self._interface_poll_loop)
        self._iface_poll_greenlet.link_exception(self._on_worker_died)
Beispiel #5
0
 def __init__(self):
     super(LabelValueIndex, self).__init__()
     self.item_ids_by_key_value = MultiDict()
     # Maps tuples of (a, b) to the set of expressions that are trivially
     # satisfied by label dicts with label a = value b.  For example,
     # trivial expressions of the form a == "b", and a in {"b", "c", ...}
     # can be evaluated by look-up in this dict.
     self.literal_exprs_by_kv = MultiDict()
     # Mapping from expression ID to any expressions that can't be
     # represented in the way described above.
     self.non_kv_expressions_by_id = {}
Beispiel #6
0
 def __init__(self):
     super(LabelValueIndex, self).__init__()
     self.item_ids_by_key_value = MultiDict()
     # Maps tuples of (a, b) to the set of expressions that are trivially
     # satisfied by label dicts with label a = value b.  For example,
     # trivial expressions of the form a == "b", and a in {"b", "c", ...}
     # can be evaluated by look-up in this dict.
     self.literal_exprs_by_kv = MultiDict()
     # Mapping from expression ID to any expressions that can't be
     # represented in the way described above.
     self.non_kv_expressions_by_id = {}
Beispiel #7
0
 def __init__(self, label_index):
     self.label_index = label_index
     self.labels_by_item_id = {}
     self.labels_by_parent_id = {}
     self.parent_ids_by_item_id = {}
     self.item_ids_by_parent_id = MultiDict()
     self._dirty_items = set()
Beispiel #8
0
    def __init__(self, config, ip_type,
                 iptables_updater,
                 workload_disp_chains,
                 host_disp_chains,
                 rules_manager,
                 fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.workload_disp_chains = workload_disp_chains
        self.host_disp_chains = host_disp_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Cache of IPs applied to host endpoints.  (I.e. any interfaces that
        # aren't workload interfaces.)
        self.host_ep_ips_by_iface = {}
        # Host interface dicts by ID.  We'll resolve these with the IPs above
        # and inject the (resolved) ones as endpoints.
        self.host_eps_by_id = {}
        # Cache of interfaces that we've resolved and injected as endpoints.
        self.resolved_host_eps = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
        self._iface_poll_greenlet = gevent.Greenlet(self._interface_poll_loop)
        self._iface_poll_greenlet.link_exception(self._on_worker_died)
Beispiel #9
0
    def __init__(self, config, ip_type, iptables_updater, dispatch_chains,
                 rules_manager, fip_manager, status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.dispatch_chains = dispatch_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
Beispiel #10
0
    def __init__(self, config, ip_type,
                 iptables_updater,
                 dispatch_chains,
                 rules_manager,
                 fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.dispatch_chains = dispatch_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
Beispiel #11
0
 def setUp(self):
     super(TestMultiDict, self).setUp()
     self.index = MultiDict()
Beispiel #12
0
class LabelInheritanceIndex(object):
    """
    Wraps a LabelIndex, adding the ability for items to inherit labels
    from a list of named parents.
    """
    def __init__(self, label_index):
        self.label_index = label_index
        self.labels_by_item_id = {}
        self.labels_by_parent_id = {}
        self.parent_ids_by_item_id = {}
        self.item_ids_by_parent_id = MultiDict()
        self._dirty_items = set()

    def on_item_update(self, item_id, labels_or_none, parents_or_none):
        """
        Called when the labels and/or parents associated with an item are
        updated.

        :param item_id: opaque hashable item ID.
        :param labels_or_none: Dict of labels, or None for a deletion.
        :param parents_or_none: List of parents, or None for a deletion.
        :return:
        """
        _log.debug("Item %s updated: %s, %s", item_id,
                   labels_or_none, parents_or_none)
        self._on_item_labels_update(item_id, labels_or_none)
        self._on_item_parents_update(item_id, parents_or_none)
        self._flush_updates()

    def _on_item_parents_update(self, item_id, parents):
        old_parents = self.parent_ids_by_item_id.get(item_id)
        if old_parents != parents:
            # Parents have changed.  Update the index from parent ID to
            # item.
            if old_parents:
                for parent_id in old_parents:
                    self.item_ids_by_parent_id.discard(parent_id, item_id)
            if parents is not None:
                for parent_id in parents:
                    self.item_ids_by_parent_id.add(parent_id, item_id)
                self.parent_ids_by_item_id[item_id] = parents
            else:
                del self.parent_ids_by_item_id[item_id]
            # Mark item dirty so that we'll re-evaluate its labels.
            self._dirty_items.add(item_id)

    def _on_item_labels_update(self, item_id, labels):
        if self.labels_by_item_id.get(item_id) != labels:
            # Labels changed, update the index and mark dirty so that we'll
            # re-evaluate its labels.
            if labels is not None:
                self.labels_by_item_id[item_id] = labels
            else:
                del self.labels_by_item_id[item_id]
            self._dirty_items.add(item_id)

    def on_parent_labels_update(self, parent_id, labels_or_none):
        """
        Called when the labels attached to a parent change.
        :param parent_id: Opaque (hashable) ID of the parent.
        :param labels_or_none: Dict of labels or None for a deletion.
        """
        _log.debug("Parent labels for %s updated: %s", parent_id,
                   labels_or_none)
        old_parent_labels = self.labels_by_parent_id.get(parent_id)
        if old_parent_labels != labels_or_none:
            # Labels changed, record the update.
            if labels_or_none is not None:
                self.labels_by_parent_id[parent_id] = labels_or_none
            else:
                del self.labels_by_parent_id[parent_id]
            # Mark all the endpoints with this parent dirty.
            self._dirty_items.update(
                self.item_ids_by_parent_id.iter_values(parent_id)
            )
        self._flush_updates()

    def _flush_updates(self):
        _log.debug("Flushing updates...")
        for item_id in self._dirty_items:
            self._flush_item(item_id)
        self._dirty_items.clear()

    def _flush_item(self, item_id):
        """
        Re-evaluates the labels for a given item ID, combining it with
        its parents and updates the wrapped label index.
        """
        try:
            item_labels = self.labels_by_item_id[item_id]
        except KeyError:
            # Labels deleted, pass that through.
            _log.debug("Flushing deletion of %s", item_id)
            self.label_index.on_labels_update(item_id, None)
        else:
            # May need to combine labels with parents.
            _log.debug("Combining labels for %s", item_id)
            combined_labels = {}
            parent_ids = self.parent_ids_by_item_id.get(item_id, [])
            _log.debug("Item %s has parents %s", item_id, parent_ids)
            for parent_id in parent_ids:
                parent_labels = self.labels_by_parent_id.get(parent_id)
                _log.debug("Parent %s has labels %s", parent_id, parent_labels)
                if parent_labels:
                    combined_labels.update(parent_labels)
            if combined_labels:
                # Some contribution from parent, need to combine.
                _log.debug("Combined labels: %s", combined_labels)
                combined_labels.update(item_labels)
            else:
                # Parent makes no contribution, just use the per-item dict.
                _log.debug("No parent labels, using item's dict %s",
                           combined_labels)
                combined_labels = item_labels
            self.label_index.on_labels_update(item_id, combined_labels)
Beispiel #13
0
class LabelValueIndex(LinearScanLabelIndex):
    """
    LabelNode index that indexes the values of labels, allowing for efficient
    (re)calculation of the matches for selectors of the form
    'a == "b" && c == "d" && ...', which are the mainline.
    """
    def __init__(self):
        super(LabelValueIndex, self).__init__()
        self.item_ids_by_key_value = MultiDict()
        # Maps tuples of (a, b) to the set of expressions that are trivially
        # satisfied by label dicts with label a = value b.  For example,
        # trivial expressions of the form a == "b", and a in {"b", "c", ...}
        # can be evaluated by look-up in this dict.
        self.literal_exprs_by_kv = MultiDict()
        # Mapping from expression ID to any expressions that can't be
        # represented in the way described above.
        self.non_kv_expressions_by_id = {}

    def on_labels_update(self, item_id, new_labels):
        """
        Called to update a particular set of labels.

        Triggers events for match changes.
        :param item_id: an opaque (hashable) ID to associate with the
               labels.  There can only be one set of labels per ID.
        :param new_labels: The labels dict to add to the index or None to
               remove it.
        """
        _log.debug("Updating labels for %s to %s", item_id, new_labels)
        # Find any old labels associated with this item_id and remove the
        # ones that have changed from the index.
        old_labels = self.labels_by_item_id.get(item_id, {})
        for k_v in old_labels.iteritems():
            k, v = k_v
            if new_labels is None or new_labels.get(k) != v:
                _log.debug("Removing old key/value (%s, %s) from index", k, v)
                self.item_ids_by_key_value.discard(k_v, item_id)
        # Check all the old matches for updates.  Record that we've already
        # re-evaluated these expressions so we can skip them later.
        seen_expr_ids = set()
        old_matches = list(self.matches_by_item_id.iter_values(item_id))
        for expr_id in old_matches:
            seen_expr_ids.add(expr_id)
            self._update_matches(expr_id, self.expressions_by_id[expr_id],
                                 item_id, new_labels)
        if new_labels is not None:
            # Spin through the new labels, storing them in the index and
            # looking for expressions of the form 'k == "v"', which we have
            # indexed.
            for k_v in new_labels.iteritems():
                _log.debug("Adding (%s, %s) to index", *k_v)
                self.item_ids_by_key_value.add(k_v, item_id)
                for expr_id in self.literal_exprs_by_kv.iter_values(k_v):
                    if expr_id in seen_expr_ids:
                        continue
                    self._store_match(expr_id, item_id)
                    seen_expr_ids.add(expr_id)
        # Spin through the remaining expressions, which we can't optimize.
        for expr_id, expr in self.non_kv_expressions_by_id.iteritems():
            if expr_id in seen_expr_ids:
                continue
            _log.debug("Checking updated labels against non-indexed expr: %s",
                       expr_id)
            self._update_matches(expr_id, expr, item_id, new_labels)
        # Finally, store the update.
        self._store_labels(item_id, new_labels)

    def on_expression_update(self, expr_id, expr):
        """
        Called to update a particular expression.

        Triggers events for match changes.
        :param expr_id: an opaque (hashable) ID to associate with the
               expression.  There can only be one expression per ID.
        :param expr: The SelectorExpression to add to the index or None to
               remove it.
        """
        old_expr = self.expressions_by_id.get(expr_id)
        if expr == old_expr:
            _log.debug("Expression %s unchanged, ignoring", expr_id)
            return

        # Remove any old value from the indexes.  We'll then add the expression
        # back in if it's suitable below.
        _log.debug("Expression %s updated to %s", expr_id, expr)
        if old_expr and isinstance(
                old_expr.expr_op,
            (LabelToLiteralEqualityNode, LabelInSetLiteralNode)):
            # Either an expression of the form a == "b", or one of the form
            # a in {"b", "c", ...}.  Undo our index for the old entry, we'll
            # then add it back in below.
            label_name = old_expr.expr_op.lhs
            if isinstance(old_expr.expr_op, LabelToLiteralEqualityNode):
                values = [old_expr.expr_op.rhs]
            else:
                values = old_expr.expr_op.rhs
            for value in values:
                _log.debug("Old expression was indexed, removing")
                k_v = label_name, value
                self.literal_exprs_by_kv.discard(k_v, expr_id)

        self.non_kv_expressions_by_id.pop(expr_id, None)

        if not expr:
            # Deletion, clean up the matches.
            for item_id in list(self.matches_by_expr_id.iter_values(expr_id)):
                _log.debug("Expression deleted, removing old match: %s",
                           item_id)
                self._update_matches(expr_id, None, item_id,
                                     self.labels_by_item_id[item_id])
        elif isinstance(expr.expr_op,
                        (LabelToLiteralEqualityNode, LabelInSetLiteralNode)):
            # Either an expression of the form a == "b", or one of the form
            # a in {"b", "c", ...}.  We can optimise these forms so that
            # they can be evaluated by an exact lookup.
            label_name = expr.expr_op.lhs
            if isinstance(expr.expr_op, LabelToLiteralEqualityNode):
                values = [expr.expr_op.rhs]
            else:
                values = expr.expr_op.rhs

            # Get the old matches as a set.  Then we can discard the items
            # that still match, leaving us with the ones that no longer
            # match.
            old_matches = set(self.matches_by_expr_id.iter_values(expr_id))
            for value in values:
                _log.debug(
                    "New expression is a LabelToLiteralEqualityNode, using "
                    "index")
                k_v = label_name, value
                for item_id in self.item_ids_by_key_value.iter_values(k_v):
                    _log.debug("From index, %s matches %s", expr_id, item_id)
                    old_matches.discard(item_id)
                    self._store_match(expr_id, item_id)
                self.literal_exprs_by_kv.add(k_v, expr_id)
            # old_matches now contains only the items that this expression
            # previously matched but no longer does.  Remove them.
            for item_id in old_matches:
                _log.debug("Removing old match %s, %s", expr_id, item_id)
                self._discard_match(expr_id, item_id)
        else:
            # The expression isn't a super-simple k == "v", let's see if we
            # can still use the index...
            required_kvs = expr.required_kvs if expr else None
            if required_kvs:
                # The expression has some required k == "v" constraints, let's
                # try to find an index that reduces the work we need to do.
                _log.debug("New expression requires these values: %s",
                           required_kvs)
                best_kv = self._find_best_index(required_kvs)
                # Scan over the best index that we found.
                old_matches = set(self.matches_by_expr_id.iter_values(expr_id))
                for item_id in self.item_ids_by_key_value.iter_values(best_kv):
                    old_matches.discard(item_id)
                    self._update_matches(expr_id, expr, item_id,
                                         self.labels_by_item_id[item_id])
                # Clean up any left-over old matches.
                for item_id in old_matches:
                    self._update_matches(expr_id, None, item_id,
                                         self.labels_by_item_id[item_id])
            else:
                # The expression was just too complex to index.  Give up and
                # do a linear scan.
                _log.debug("%s too complex to use indexes, doing linear scan",
                           expr_id)
                self._scan_all_labels(expr_id, expr)
            self.non_kv_expressions_by_id[expr_id] = expr
        # Finally, store the update.
        self._store_expression(expr_id, expr)

    def _find_best_index(self, required_kvs):
        """
        Finds the smallest index for the given set of key/value requirements.

        For example, an expression "env == 'prod' && type == 'foo'" would have
        requirements [("env", "prod"), ("type", "foo")].  Suppose type=="foo"
        only applies to a handful of items but env=="prod" applies to many;
        this method would return ("type", "foo") as the best index.

        :returns the key, value tuple for the best index to use.
        """
        min_kv = None
        min_num = None
        for k_v in required_kvs:
            num = self.item_ids_by_key_value.num_items(k_v)
            if min_num is None or num < min_num:
                min_kv = k_v
                min_num = num
                if num < 10:
                    # Good enough, let's get on with evaluating the
                    # expressions rather than spending more time looking for
                    # a better index.
                    break
        _log.debug("Best index: %s, %s items", min_kv, min_num)
        return min_kv
Beispiel #14
0
class LinearScanLabelIndex(object):
    """
    A label index matches a set of SelectorExpressions against a set of
    label dicts.  As the matches between the two collections change,
    it triggers calls to on_match_started/on_match_stopped.

    LabelNode dicts are identified by their "item_id", which is an opaque
    (hashable) ID for the item that the labels apply to.

    Similarly, selector expressions are identified by an opaque expr_id.

    A simple implementation of a label index.  Every update is handled by
    a full linear scan.

    This class has a few purposes:

    - it provides a benchmark against which other implementations can be
      measured
    - since it's simple, it's useful for comparative testing; any
      other label index implementation should give the same end result.
    - it's a base class for more advanced implementations.
    """
    def __init__(self):
        # Cache of raw label dicts by item_id.
        self.labels_by_item_id = {}
        # All expressions by ID.
        self.expressions_by_id = {}
        # Map from expression ID to matching item_ids.
        self.matches_by_expr_id = MultiDict()
        self.matches_by_item_id = MultiDict()

    def on_expression_update(self, expr_id, expr):
        """
        Called to update a particular expression.

        Triggers events for match changes.
        :param expr_id: an opaque (hashable) ID to associate with the
               expression.  There can only be one expression per ID.
        :param expr: The SelectorExpression to add to the index or None to
               remove it.
        """
        _log.debug("Expression %s updated to %s", expr_id, expr)
        self._scan_all_labels(expr_id, expr)
        self._store_expression(expr_id, expr)

    def on_labels_update(self, item_id, new_labels):
        """
        Called to update a particular set of labels.

        Triggers events for match changes.
        :param item_id: an opaque (hashable) ID to associate with the
               labels.  There can only be one set of labels per ID.
        :param new_labels: The labels dict to add to the index or None to
               remove it.
        """
        _log.debug("Labels for %s now %s", item_id, new_labels)
        self._scan_all_expressions(item_id, new_labels)
        self._store_labels(item_id, new_labels)

    def _scan_all_labels(self, expr_id, expr):
        """
        Check the given expression against all label dicts and emit
        events for changes in the matching labels.
        """
        _log.debug("Doing full label scan against expression %s", expr_id)
        for item_id, label_values in self.labels_by_item_id.iteritems():
            self._update_matches(expr_id, expr, item_id, label_values)

    def _scan_all_expressions(self, item_id, new_labels):
        """
        Check the given labels against all expressions and emit
        events for changes in the matching labels.
        """
        _log.debug("Doing full expression scan against item %s", item_id)
        for expr_id, expr in self.expressions_by_id.iteritems():
            self._update_matches(expr_id, expr, item_id, new_labels)

    def _store_expression(self, expr_id, expr):
        """Updates expressions_by_id with the new value for an expression."""
        if expr is not None:
            self.expressions_by_id[expr_id] = expr
        else:
            self.expressions_by_id.pop(expr_id, None)

    def _store_labels(self, item_id, new_labels):
        """Updates labels_by_item_id with the new labels for an item."""
        if new_labels is not None:
            self.labels_by_item_id[item_id] = new_labels
        else:
            self.labels_by_item_id.pop(item_id, None)

    def _update_matches(self, expr_id, expr, item_id, label_values):
        """
        (Re-)evaluates the given expression against the given labels and
        stores the result.
        """
        _log.debug("Re-evaluating %s against %s (%s)", expr_id, item_id,
                   label_values)
        if expr is not None and label_values is not None:
            now_matches = expr.evaluate(label_values)
            _log.debug("After evaluation, now matches: %s", now_matches)
        else:
            _log.debug("Expr or labels missing: no match")
            now_matches = False
        # Update the index and generate events.  These methods are idempotent
        # so they'll ignore duplicate updates.
        if now_matches:
            self._store_match(expr_id, item_id)
        else:
            self._discard_match(expr_id, item_id)

    def _store_match(self, expr_id, item_id):
        """
        Stores that an expression matches an item.

        Calls on_match_started() as a side-effect. Idempotent, does
        nothing if the match is already recorded.
        """
        previously_matched = self.matches_by_expr_id.contains(expr_id, item_id)
        if not previously_matched:
            _log.debug("%s now matches: %s", expr_id, item_id)
            self.matches_by_expr_id.add(expr_id, item_id)
            self.matches_by_item_id.add(item_id, expr_id)
            self.on_match_started(expr_id, item_id)

    def _discard_match(self, expr_id, item_id):
        """
        Stores that an expression does not match an item.

        Calls on_match_stopped() as a side-effect.  Idempotent, does
        nothing if the non-match is already recorded.
        """
        previously_matched = self.matches_by_expr_id.contains(expr_id, item_id)
        if previously_matched:
            _log.debug("%s no longer matches %s", expr_id, item_id)
            self.matches_by_expr_id.discard(expr_id, item_id)
            self.matches_by_item_id.discard(item_id, expr_id)
            self.on_match_stopped(expr_id, item_id)

    def on_match_started(self, expr_id, item_id):
        """
        Called when an expression starts matching a particular set of
        labels.

        Intended to be assigned/overriden.
        """
        _log.debug("SelectorExpression %s now matches item %s", expr_id,
                   item_id)

    def on_match_stopped(self, expr_id, item_id):
        """
        Called when an expression stops matching a particular set of
        labels.

        Intended to be assigned/overriden.
        """
        _log.debug("SelectorExpression %s no longer matches item %s", expr_id,
                   item_id)
Beispiel #15
0
class LinearScanLabelIndex(object):
    """
    A label index matches a set of SelectorExpressions against a set of
    label dicts.  As the matches between the two collections change,
    it triggers calls to on_match_started/on_match_stopped.

    LabelNode dicts are identified by their "item_id", which is an opaque
    (hashable) ID for the item that the labels apply to.

    Similarly, selector expressions are identified by an opaque expr_id.

    A simple implementation of a label index.  Every update is handled by
    a full linear scan.

    This class has a few purposes:

    - it provides a benchmark against which other implementations can be
      measured
    - since it's simple, it's useful for comparative testing; any
      other label index implementation should give the same end result.
    - it's a base class for more advanced implementations.
    """

    def __init__(self):
        # Cache of raw label dicts by item_id.
        self.labels_by_item_id = {}
        # All expressions by ID.
        self.expressions_by_id = {}
        # Map from expression ID to matching item_ids.
        self.matches_by_expr_id = MultiDict()
        self.matches_by_item_id = MultiDict()

    def on_expression_update(self, expr_id, expr):
        """
        Called to update a particular expression.

        Triggers events for match changes.
        :param expr_id: an opaque (hashable) ID to associate with the
               expression.  There can only be one expression per ID.
        :param expr: The SelectorExpression to add to the index or None to
               remove it.
        """
        _log.debug("Expression %s updated to %s", expr_id, expr)
        self._scan_all_labels(expr_id, expr)
        self._store_expression(expr_id, expr)

    def on_labels_update(self, item_id, new_labels):
        """
        Called to update a particular set of labels.

        Triggers events for match changes.
        :param item_id: an opaque (hashable) ID to associate with the
               labels.  There can only be one set of labels per ID.
        :param new_labels: The labels dict to add to the index or None to
               remove it.
        """
        _log.debug("Labels for %s now %s", item_id, new_labels)
        self._scan_all_expressions(item_id, new_labels)
        self._store_labels(item_id, new_labels)

    def _scan_all_labels(self, expr_id, expr):
        """
        Check the given expression against all label dicts and emit
        events for changes in the matching labels.
        """
        _log.debug("Doing full label scan against expression %s", expr_id)
        for item_id, label_values in self.labels_by_item_id.iteritems():
            self._update_matches(expr_id, expr, item_id, label_values)

    def _scan_all_expressions(self, item_id, new_labels):
        """
        Check the given labels against all expressions and emit
        events for changes in the matching labels.
        """
        _log.debug("Doing full expression scan against item %s", item_id)
        for expr_id, expr in self.expressions_by_id.iteritems():
            self._update_matches(expr_id, expr, item_id, new_labels)

    def _store_expression(self, expr_id, expr):
        """Updates expressions_by_id with the new value for an expression."""
        if expr is not None:
            self.expressions_by_id[expr_id] = expr
        else:
            self.expressions_by_id.pop(expr_id, None)

    def _store_labels(self, item_id, new_labels):
        """Updates labels_by_item_id with the new labels for an item."""
        if new_labels is not None:
            self.labels_by_item_id[item_id] = new_labels
        else:
            self.labels_by_item_id.pop(item_id, None)

    def _update_matches(self, expr_id, expr, item_id, label_values):
        """
        (Re-)evaluates the given expression against the given labels and
        stores the result.
        """
        _log.debug("Re-evaluating %s against %s (%s)", expr_id, item_id,
                   label_values)
        if expr is not None and label_values is not None:
            now_matches = expr.evaluate(label_values)
            _log.debug("After evaluation, now matches: %s", now_matches)
        else:
            _log.debug("Expr or labels missing: no match")
            now_matches = False
        # Update the index and generate events.  These methods are idempotent
        # so they'll ignore duplicate updates.
        if now_matches:
            self._store_match(expr_id, item_id)
        else:
            self._discard_match(expr_id, item_id)

    def _store_match(self, expr_id, item_id):
        """
        Stores that an expression matches an item.

        Calls on_match_started() as a side-effect. Idempotent, does
        nothing if the match is already recorded.
        """
        previously_matched = self.matches_by_expr_id.contains(expr_id, item_id)
        if not previously_matched:
            _log.debug("%s now matches: %s", expr_id, item_id)
            self.matches_by_expr_id.add(expr_id, item_id)
            self.matches_by_item_id.add(item_id, expr_id)
            self.on_match_started(expr_id, item_id)

    def _discard_match(self, expr_id, item_id):
        """
        Stores that an expression does not match an item.

        Calls on_match_stopped() as a side-effect.  Idempotent, does
        nothing if the non-match is already recorded.
        """
        previously_matched = self.matches_by_expr_id.contains(expr_id, item_id)
        if previously_matched:
            _log.debug("%s no longer matches %s", expr_id, item_id)
            self.matches_by_expr_id.discard(expr_id, item_id)
            self.matches_by_item_id.discard(item_id, expr_id)
            self.on_match_stopped(expr_id, item_id)

    def on_match_started(self, expr_id, item_id):
        """
        Called when an expression starts matching a particular set of
        labels.

        Intended to be assigned/overriden.
        """
        _log.debug("SelectorExpression %s now matches item %s",
                   expr_id, item_id)

    def on_match_stopped(self, expr_id, item_id):
        """
        Called when an expression stops matching a particular set of
        labels.

        Intended to be assigned/overriden.
        """
        _log.debug("SelectorExpression %s no longer matches item %s",
                   expr_id, item_id)
Beispiel #16
0
class LabelInheritanceIndex(object):
    """
    Wraps a LabelIndex, adding the ability for items to inherit labels
    from a list of named parents.
    """
    def __init__(self, label_index):
        self.label_index = label_index
        self.labels_by_item_id = {}
        self.labels_by_parent_id = {}
        self.parent_ids_by_item_id = {}
        self.item_ids_by_parent_id = MultiDict()
        self._dirty_items = set()

    def on_item_update(self, item_id, labels_or_none, parents_or_none):
        """
        Called when the labels and/or parents associated with an item are
        updated.

        :param item_id: opaque hashable item ID.
        :param labels_or_none: Dict of labels, or None for a deletion.
        :param parents_or_none: List of parents, or None for a deletion.
        :return:
        """
        _log.debug("Item %s updated: %s, %s", item_id, labels_or_none,
                   parents_or_none)
        self._on_item_labels_update(item_id, labels_or_none)
        self._on_item_parents_update(item_id, parents_or_none)
        self._flush_updates()

    def _on_item_parents_update(self, item_id, parents):
        old_parents = self.parent_ids_by_item_id.get(item_id)
        if old_parents != parents:
            # Parents have changed.  Update the index from parent ID to
            # item.
            if old_parents:
                for parent_id in old_parents:
                    self.item_ids_by_parent_id.discard(parent_id, item_id)
            if parents is not None:
                for parent_id in parents:
                    self.item_ids_by_parent_id.add(parent_id, item_id)
                self.parent_ids_by_item_id[item_id] = parents
            else:
                del self.parent_ids_by_item_id[item_id]
            # Mark item dirty so that we'll re-evaluate its labels.
            self._dirty_items.add(item_id)

    def _on_item_labels_update(self, item_id, labels):
        if self.labels_by_item_id.get(item_id) != labels:
            # Labels changed, update the index and mark dirty so that we'll
            # re-evaluate its labels.
            if labels is not None:
                self.labels_by_item_id[item_id] = labels
            else:
                del self.labels_by_item_id[item_id]
            self._dirty_items.add(item_id)

    def on_parent_labels_update(self, parent_id, labels_or_none):
        """
        Called when the labels attached to a parent change.
        :param parent_id: Opaque (hashable) ID of the parent.
        :param labels_or_none: Dict of labels or None for a deletion.
        """
        _log.debug("Parent labels for %s updated: %s", parent_id,
                   labels_or_none)
        old_parent_labels = self.labels_by_parent_id.get(parent_id)
        if old_parent_labels != labels_or_none:
            # Labels changed, record the update.
            if labels_or_none is not None:
                self.labels_by_parent_id[parent_id] = labels_or_none
            else:
                del self.labels_by_parent_id[parent_id]
            # Mark all the endpoints with this parent dirty.
            self._dirty_items.update(
                self.item_ids_by_parent_id.iter_values(parent_id))
        self._flush_updates()

    def _flush_updates(self):
        _log.debug("Flushing updates...")
        for item_id in self._dirty_items:
            self._flush_item(item_id)
        self._dirty_items.clear()

    def _flush_item(self, item_id):
        """
        Re-evaluates the labels for a given item ID, combining it with
        its parents and updates the wrapped label index.
        """
        try:
            item_labels = self.labels_by_item_id[item_id]
        except KeyError:
            # Labels deleted, pass that through.
            _log.debug("Flushing deletion of %s", item_id)
            self.label_index.on_labels_update(item_id, None)
        else:
            # May need to combine labels with parents.
            _log.debug("Combining labels for %s", item_id)
            combined_labels = {}
            parent_ids = self.parent_ids_by_item_id.get(item_id, [])
            _log.debug("Item %s has parents %s", item_id, parent_ids)
            for parent_id in parent_ids:
                parent_labels = self.labels_by_parent_id.get(parent_id)
                _log.debug("Parent %s has labels %s", parent_id, parent_labels)
                if parent_labels:
                    combined_labels.update(parent_labels)
            if combined_labels:
                # Some contribution from parent, need to combine.
                _log.debug("Combined labels: %s", combined_labels)
                combined_labels.update(item_labels)
            else:
                # Parent makes no contribution, just use the per-item dict.
                _log.debug("No parent labels, using item's dict %s",
                           combined_labels)
                combined_labels = item_labels
            self.label_index.on_labels_update(item_id, combined_labels)
Beispiel #17
0
class LabelValueIndex(LinearScanLabelIndex):
    """
    LabelNode index that indexes the values of labels, allowing for efficient
    (re)calculation of the matches for selectors of the form
    'a == "b" && c == "d" && ...', which are the mainline.
    """
    def __init__(self):
        super(LabelValueIndex, self).__init__()
        self.item_ids_by_key_value = MultiDict()
        # Maps tuples of (a, b) to the set of expressions that are trivially
        # satisfied by label dicts with label a = value b.  For example,
        # trivial expressions of the form a == "b", and a in {"b", "c", ...}
        # can be evaluated by look-up in this dict.
        self.literal_exprs_by_kv = MultiDict()
        # Mapping from expression ID to any expressions that can't be
        # represented in the way described above.
        self.non_kv_expressions_by_id = {}

    def on_labels_update(self, item_id, new_labels):
        """
        Called to update a particular set of labels.

        Triggers events for match changes.
        :param item_id: an opaque (hashable) ID to associate with the
               labels.  There can only be one set of labels per ID.
        :param new_labels: The labels dict to add to the index or None to
               remove it.
        """
        _log.debug("Updating labels for %s to %s", item_id, new_labels)
        # Find any old labels associated with this item_id and remove the
        # ones that have changed from the index.
        old_labels = self.labels_by_item_id.get(item_id, {})
        for k_v in old_labels.iteritems():
            k, v = k_v
            if new_labels is None or new_labels.get(k) != v:
                _log.debug("Removing old key/value (%s, %s) from index", k, v)
                self.item_ids_by_key_value.discard(k_v, item_id)
        # Check all the old matches for updates.  Record that we've already
        # re-evaluated these expressions so we can skip them later.
        seen_expr_ids = set()
        old_matches = list(self.matches_by_item_id.iter_values(item_id))
        for expr_id in old_matches:
            seen_expr_ids.add(expr_id)
            self._update_matches(expr_id, self.expressions_by_id[expr_id],
                                 item_id, new_labels)
        if new_labels is not None:
            # Spin through the new labels, storing them in the index and
            # looking for expressions of the form 'k == "v"', which we have
            # indexed.
            for k_v in new_labels.iteritems():
                _log.debug("Adding (%s, %s) to index", *k_v)
                self.item_ids_by_key_value.add(k_v, item_id)
                for expr_id in self.literal_exprs_by_kv.iter_values(k_v):
                    if expr_id in seen_expr_ids:
                        continue
                    self._store_match(expr_id, item_id)
                    seen_expr_ids.add(expr_id)
        # Spin through the remaining expressions, which we can't optimize.
        for expr_id, expr in self.non_kv_expressions_by_id.iteritems():
            if expr_id in seen_expr_ids:
                continue
            _log.debug("Checking updated labels against non-indexed expr: %s",
                       expr_id)
            self._update_matches(expr_id, expr, item_id, new_labels)
        # Finally, store the update.
        self._store_labels(item_id, new_labels)

    def on_expression_update(self, expr_id, expr):
        """
        Called to update a particular expression.

        Triggers events for match changes.
        :param expr_id: an opaque (hashable) ID to associate with the
               expression.  There can only be one expression per ID.
        :param expr: The SelectorExpression to add to the index or None to
               remove it.
        """
        old_expr = self.expressions_by_id.get(expr_id)
        if expr == old_expr:
            _log.debug("Expression %s unchanged, ignoring", expr_id)
            return

        # Remove any old value from the indexes.  We'll then add the expression
        # back in if it's suitable below.
        _log.debug("Expression %s updated to %s", expr_id, expr)
        if old_expr and isinstance(old_expr.expr_op, (LabelToLiteralEqualityNode,
                                                      LabelInSetLiteralNode)):
            # Either an expression of the form a == "b", or one of the form
            # a in {"b", "c", ...}.  Undo our index for the old entry, we'll
            # then add it back in below.
            label_name = old_expr.expr_op.lhs
            if isinstance(old_expr.expr_op, LabelToLiteralEqualityNode):
                values = [old_expr.expr_op.rhs]
            else:
                values = old_expr.expr_op.rhs
            for value in values:
                _log.debug("Old expression was indexed, removing")
                k_v = label_name, value
                self.literal_exprs_by_kv.discard(k_v, expr_id)

        self.non_kv_expressions_by_id.pop(expr_id, None)

        if not expr:
            # Deletion, clean up the matches.
            for item_id in list(self.matches_by_expr_id.iter_values(expr_id)):
                _log.debug("Expression deleted, removing old match: %s",
                           item_id)
                self._update_matches(expr_id, None, item_id,
                                     self.labels_by_item_id[item_id])
        elif isinstance(expr.expr_op, (LabelToLiteralEqualityNode,
                                       LabelInSetLiteralNode)):
            # Either an expression of the form a == "b", or one of the form
            # a in {"b", "c", ...}.  We can optimise these forms so that
            # they can be evaluated by an exact lookup.
            label_name = expr.expr_op.lhs
            if isinstance(expr.expr_op, LabelToLiteralEqualityNode):
                values = [expr.expr_op.rhs]
            else:
                values = expr.expr_op.rhs

            # Get the old matches as a set.  Then we can discard the items
            # that still match, leaving us with the ones that no longer
            # match.
            old_matches = set(self.matches_by_expr_id.iter_values(expr_id))
            for value in values:
                _log.debug("New expression is a LabelToLiteralEqualityNode, using "
                           "index")
                k_v = label_name, value
                for item_id in self.item_ids_by_key_value.iter_values(k_v):
                    _log.debug("From index, %s matches %s", expr_id, item_id)
                    old_matches.discard(item_id)
                    self._store_match(expr_id, item_id)
                self.literal_exprs_by_kv.add(k_v, expr_id)
            # old_matches now contains only the items that this expression
            # previously matched but no longer does.  Remove them.
            for item_id in old_matches:
                _log.debug("Removing old match %s, %s", expr_id, item_id)
                self._discard_match(expr_id, item_id)
        else:
            # The expression isn't a super-simple k == "v", let's see if we
            # can still use the index...
            required_kvs = expr.required_kvs if expr else None
            if required_kvs:
                # The expression has some required k == "v" constraints, let's
                # try to find an index that reduces the work we need to do.
                _log.debug("New expression requires these values: %s",
                           required_kvs)
                best_kv = self._find_best_index(required_kvs)
                # Scan over the best index that we found.
                old_matches = set(self.matches_by_expr_id.iter_values(expr_id))
                for item_id in self.item_ids_by_key_value.iter_values(best_kv):
                    old_matches.discard(item_id)
                    self._update_matches(expr_id, expr, item_id,
                                         self.labels_by_item_id[item_id])
                # Clean up any left-over old matches.
                for item_id in old_matches:
                    self._update_matches(expr_id, None, item_id,
                                         self.labels_by_item_id[item_id])
            else:
                # The expression was just too complex to index.  Give up and
                # do a linear scan.
                _log.debug("%s too complex to use indexes, doing linear scan",
                           expr_id)
                self._scan_all_labels(expr_id, expr)
            self.non_kv_expressions_by_id[expr_id] = expr
        # Finally, store the update.
        self._store_expression(expr_id, expr)

    def _find_best_index(self, required_kvs):
        """
        Finds the smallest index for the given set of key/value requirements.

        For example, an expression "env == 'prod' && type == 'foo'" would have
        requirements [("env", "prod"), ("type", "foo")].  Suppose type=="foo"
        only applies to a handful of items but env=="prod" applies to many;
        this method would return ("type", "foo") as the best index.

        :returns the key, value tuple for the best index to use.
        """
        min_kv = None
        min_num = None
        for k_v in required_kvs:
            num = self.item_ids_by_key_value.num_items(k_v)
            if min_num is None or num < min_num:
                min_kv = k_v
                min_num = num
                if num < 10:
                    # Good enough, let's get on with evaluating the
                    # expressions rather than spending more time looking for
                    # a better index.
                    break
        _log.debug("Best index: %s, %s items", min_kv, min_num)
        return min_kv
Beispiel #18
0
class EndpointManager(ReferenceManager):
    def __init__(self, config, ip_type, iptables_updater, workload_disp_chains,
                 host_disp_chains, rules_manager, fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.workload_disp_chains = workload_disp_chains
        self.host_disp_chains = host_disp_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Cache of IPs applied to host endpoints.  (I.e. any interfaces that
        # aren't workload interfaces.)
        self.host_ep_ips_by_iface = {}
        # Host interface dicts by ID.  We'll resolve these with the IPs above
        # and inject the (resolved) ones as endpoints.
        self.host_eps_by_id = {}
        # Cache of interfaces that we've resolved and injected as endpoints.
        self.resolved_host_eps = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
        self._iface_poll_greenlet = gevent.Greenlet(self._interface_poll_loop)
        self._iface_poll_greenlet.link_exception(self._on_worker_died)

    def _on_actor_started(self):
        _log.info("Endpoint manager started, spawning interface poll worker.")
        self._iface_poll_greenlet.start()

    def _create(self, combined_id):
        """
        Overrides ReferenceManager._create()
        """
        if isinstance(combined_id, WloadEndpointId):
            return WorkloadEndpoint(self.config, combined_id, self.ip_type,
                                    self.iptables_updater,
                                    self.workload_disp_chains, self.rules_mgr,
                                    self.fip_manager, self.status_reporter)
        elif isinstance(combined_id, ResolvedHostEndpointId):
            return HostEndpoint(self.config, combined_id, self.ip_type,
                                self.iptables_updater, self.host_disp_chains,
                                self.rules_mgr, self.fip_manager,
                                self.status_reporter)
        else:
            raise RuntimeError("Unknown ID type: %s" % combined_id)

    @actor_message()
    def on_tier_data_update(self, tier, data):
        """
        Message received when the metadata for a policy tier is updated
        in etcd.

        :param str tier: The name of the tier.
        :param dict|NoneType data: The dict or None, for a deletion.
        """
        _log.debug("Data for policy tier %s updated to %s", tier, data)

        # Currently, the only data we care about is the order.
        order = None if data is None else data["order"]
        if self.tier_orders.get(tier) == order:
            _log.debug("No change, ignoring")
            return

        if order is not None:
            self.tier_orders[tier] = order
        else:
            del self.tier_orders[tier]

        new_tier_sequence = sorted(self.tier_orders.iterkeys(),
                                   key=lambda k: (self.tier_orders[k], k))
        if self.tier_sequence != new_tier_sequence:
            _log.info("Sequence of profile tiers changed, refreshing all "
                      "endpoints")
            self.tier_sequence = new_tier_sequence
            self.endpoints_with_dirty_policy.update(
                self.endpoints_by_id.keys())
            self._update_dirty_policy()

    @actor_message()
    def on_prof_labels_set(self, profile_id, labels):
        _log.debug("Profile labels updated for %s: %s", profile_id, labels)
        # Defer to the label index, which will call us back synchronously
        # with any match changes.
        self._label_inherit_idx.on_parent_labels_update(profile_id, labels)
        # Process any match changes that we've recorded in the callbacks.
        self._update_dirty_policy()

    @actor_message()
    def on_policy_selector_update(self, policy_id, selector_or_none,
                                  order_or_none):
        _log.debug("Policy %s selector updated to %s (%s)", policy_id,
                   selector_or_none, order_or_none)
        # Defer to the label index, which will call us back synchronously
        # via on_policy_match_started and on_policy_match_stopped.
        self.policy_index.on_expression_update(policy_id, selector_or_none)

        # Before we update the policies, check if the order has changed,
        # which would mean we need to refresh all endpoints with this policy
        # too.
        if order_or_none != self.profile_orders.get(policy_id):
            if order_or_none is not None:
                self.profile_orders[policy_id] = order_or_none
            else:
                del self.profile_orders[policy_id]
            self.endpoints_with_dirty_policy.update(
                self.policy_index.matches_by_expr_id.iter_values(policy_id))

        # Finally, flush any updates to our waiting endpoints.
        self._update_dirty_policy()

    def on_policy_match_started(self, expr_id, item_id):
        """Called by the label index when a new match is started.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s now applies to endpoint %s", expr_id, item_id)
        self.pol_ids_by_ep_id.add(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def on_policy_match_stopped(self, expr_id, item_id):
        """Called by the label index when a match stops.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s no longer applies to endpoint %s", expr_id,
                  item_id)
        self.pol_ids_by_ep_id.discard(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def _on_object_started(self, endpoint_id, obj):
        """
        Callback from a LocalEndpoint to report that it has started.
        Overrides ReferenceManager._on_object_started
        """
        ep = self.endpoints_by_id.get(endpoint_id)
        obj.on_endpoint_update(ep, async=True)
        self._update_tiered_policy(endpoint_id)

    @actor_message()
    def on_datamodel_in_sync(self):
        if not self._data_model_in_sync:
            _log.info(
                "%s: First time we've been in-sync with the datamodel,"
                "sending snapshot to DispatchChains and FIPManager.", self)
            self._data_model_in_sync = True

            # Tell the dispatch chains about the local endpoints in advance so
            # that we don't flap the dispatch chain at start-of-day.  Note:
            # the snapshot may contain information that is ahead of the
            # state that our individual LocalEndpoint actors are sending to the
            # DispatchChains actor.  That is OK!  The worst that can happen is
            # that a LocalEndpoint undoes part of our update and then goes on
            # to re-apply the update when it catches up to the snapshot.
            workload_ifaces = set()
            host_eps = set()
            for if_name, ep_id in self.endpoint_id_by_iface_name.iteritems():
                if isinstance(ep_id, WloadEndpointId):
                    workload_ifaces.add(if_name)
                else:
                    host_eps.add(if_name)
            self.workload_disp_chains.apply_snapshot(
                frozenset(workload_ifaces), async=True)
            self.host_disp_chains.apply_snapshot(frozenset(host_eps),
                                                 async=True)
            self._update_dirty_policy()

            nat_maps = {}
            for ep_id, ep in self.endpoints_by_id.iteritems():
                if ep_id in self.local_endpoint_ids:
                    nat_map = ep.get(nat_key(self.ip_type), None)
                    if nat_map:
                        nat_maps[ep_id] = nat_map
            self.fip_manager.apply_snapshot(nat_maps, async=True)

    @actor_message()
    def on_host_ep_update(self, combined_id, data):
        if combined_id.host != self.config.HOSTNAME:
            _log.debug("Skipping endpoint %s; not on our host.", combined_id)
            return
        if data is not None:
            self.host_eps_by_id[combined_id] = data
        else:
            self.host_eps_by_id.pop(combined_id, None)
        self._resolve_host_eps()

    @actor_message()
    def on_endpoint_update(self, endpoint_id, endpoint, force_reprogram=False):
        """
        Event to indicate that an endpoint has been updated (including
        creation or deletion).

        :param EndpointId endpoint_id: The endpoint ID in question.
        :param dict[str]|NoneType endpoint: Dictionary of all endpoint
            data or None if the endpoint is to be deleted.
        """
        if endpoint_id.host != self.config.HOSTNAME:
            _log.debug("Skipping endpoint %s; not on our host.", endpoint_id)
            return

        old_ep = self.endpoints_by_id.get(endpoint_id, {})
        old_iface_name = old_ep.get("name")
        new_iface_name = (endpoint or {}).get("name")

        if (old_iface_name is not None and new_iface_name is not None
                and old_iface_name != new_iface_name):
            # Special-case: if the interface name of an active endpoint
            # changes we need to clean up routes and iptables and start from
            # scratch.  Force that through the deletion path so that we don't
            # introduce any more complexity in LocalEndpoint.
            _log.info(
                "Name of interface for endpoint %s changed from %s "
                "to %s.  Forcing a delete/re-add.", endpoint_id,
                old_iface_name, new_iface_name)
            self._on_endpoint_update_internal(endpoint_id, None,
                                              force_reprogram)

        self._on_endpoint_update_internal(endpoint_id, endpoint,
                                          force_reprogram)

    def _on_endpoint_update_internal(self,
                                     endpoint_id,
                                     endpoint,
                                     force_reprogram=False):
        """Handles a single update or deletion of an endpoint.

        Increfs/decrefs the actor as appropriate and forwards on the update
        if the endpoint is active.

        :param EndpointId endpoint_id: The endpoint ID in question.
        :param dict[str]|NoneType endpoint: Dictionary of all endpoint
            data or None if the endpoint is to be deleted.
        """
        if self._is_starting_or_live(endpoint_id):
            # Local endpoint thread is running; tell it of the change.
            _log.info("Update for live endpoint %s", endpoint_id)
            self.objects_by_id[endpoint_id].on_endpoint_update(
                endpoint, force_reprogram=force_reprogram, async=True)

        old_ep = self.endpoints_by_id.pop(endpoint_id, {})
        # Interface name shouldn't change but popping it now is correct for
        # deletes and we add it back in below on create/modify.
        old_iface_name = old_ep.get("name")
        self.endpoint_id_by_iface_name.pop(old_iface_name, None)
        if endpoint is None:
            # Deletion. Remove from the list.
            _log.info("Endpoint %s deleted", endpoint_id)
            if endpoint_id in self.local_endpoint_ids:
                self.decref(endpoint_id)
                self.local_endpoint_ids.remove(endpoint_id)
                self._label_inherit_idx.on_item_update(endpoint_id, None, None)
                assert endpoint_id not in self.pol_ids_by_ep_id
        else:
            # Creation or modification
            _log.info("Endpoint %s modified or created", endpoint_id)
            self.endpoints_by_id[endpoint_id] = endpoint
            self.endpoint_id_by_iface_name[endpoint["name"]] = endpoint_id
            if endpoint_id not in self.local_endpoint_ids:
                # This will trigger _on_object_activated to pass the endpoint
                # we just saved off to the endpoint.
                _log.debug("Endpoint wasn't known before, increffing it")
                self.local_endpoint_ids.add(endpoint_id)
                self.get_and_incref(endpoint_id)
            self._label_inherit_idx.on_item_update(
                endpoint_id, endpoint.get("labels", {}),
                endpoint.get("profile_ids", []))

        self._update_dirty_policy()

    @actor_message()
    def on_interface_update(self, name, iface_up):
        """
        Called when an interface is created or changes state.

        The interface may be any interface on the host, not necessarily
        one managed by any endpoint of this server.
        """
        try:
            endpoint_id = self.endpoint_id_by_iface_name[name]
        except KeyError:
            _log.debug("Update on interface %s that we do not care about",
                       name)
        else:
            _log.info("Endpoint %s received interface update for %s",
                      endpoint_id, name)
            if self._is_starting_or_live(endpoint_id):
                # LocalEndpoint is running, so tell it about the change.
                ep = self.objects_by_id[endpoint_id]
                ep.on_interface_update(iface_up, async=True)

    def _interface_poll_loop(self):
        """Greenlet: Polls host endpoints for changes to their IP addresses.

        Sends updates to the EndpointManager via the _on_iface_ips_update()
        message.

        If polling is disabled, then it reads the interfaces once and then
        stops.
        """
        known_interfaces = {}
        while True:
            known_interfaces = self._poll_interfaces(known_interfaces)
            if self.config.HOST_IF_POLL_INTERVAL_SECS <= 0:
                _log.info("Host interface polling disabled, stopping after "
                          "initial read. Further changes to host endpoint "
                          "IPs will be ignored.")
                break
            gevent.sleep(self.config.HOST_IF_POLL_INTERVAL_SECS)

    def _poll_interfaces(self, known_interfaces):
        """Does a single poll of the host interfaces, looking for IP changes.

        Sends updates to the EndpointManager via the _on_iface_ips_update()
        message.

        This is broken out form the loop above to make it easier to test.

        :param known_interfaces:
        :return:
        """
        # We only care about host interfaces, not workload ones.
        exclude_prefixes = self.config.IFACE_PREFIX
        # Get the IPs for each interface.
        ips_by_iface = devices.list_ips_by_iface(self.ip_type)
        for iface, ips in ips_by_iface.items():
            ignore_iface = any(
                iface.startswith(prefix) for prefix in exclude_prefixes)
            if ignore_iface:
                # Ignore non-host interfaces.
                ips_by_iface.pop(iface)
            else:
                # Compare with the set of IPs that were there before.
                # We pop interfaces that we see so that we can clean up
                # deletions below.
                old_ips = known_interfaces.pop(iface, None)
                if old_ips != ips:
                    _log.debug("IPs of interface %s changed to %s", iface, ips)
                    self._on_iface_ips_update(iface, ips, async=True)
        # Clean up deletions.  Anything left in known_interfaces has
        # been deleted.
        for iface, ips in known_interfaces.iteritems():
            self._on_iface_ips_update(iface, None, async=True)
        # Update our cache of known interfaces for the next loop.
        return ips_by_iface

    @actor_message()
    def _on_iface_ips_update(self, iface_name, ip_addrs):
        """Message sent by _poll_interface_ips when it detects a change.

        :param iface_name: Name of the interface that has been updated.
        :param ip_addrs: set of IP addresses, or None if the interface no
               longer exists (or has no IPs).
        """
        _log.info("Interface %s now has IPs %s", iface_name, ip_addrs)
        if ip_addrs is not None:
            self.host_ep_ips_by_iface[iface_name] = ip_addrs
        else:
            self.host_ep_ips_by_iface.pop(iface_name, None)
        # Since changes to IPs can change which host endpoint objects apply to
        # which interfaces, we need to resolve IPs and host endpoints.
        self._resolve_host_eps()

    def _resolve_host_eps(self):
        """Resolves the host endpoint data we've learned from etcd with
        IP addresses and interface names learned from the kernel.

        Host interfaces that have matching IPs get combined with interface
        name learned from the kernel and updated via on_endpoint_update().

        In the case where multiple interfaces have the same IP address,
        a copy of the host endpoint will be resolved with each interface.
        """
        # Invert the interface name to IP mapping to allow us to do an IP to
        # interface name lookup.
        iface_names_by_ip = defaultdict(set)
        for iface, ips in self.host_ep_ips_by_iface.iteritems():
            for ip in ips:
                iface_names_by_ip[ip].add(iface)
        # Iterate over the host endpoints, looking for corresponding IPs.
        resolved_ifaces = {}
        iface_name_to_id = {}
        # For repeatability, we sort the endpoint data.  We don't care what
        # the sort order is, only that it's stable so we just use the repr()
        # of the ID.
        for combined_id, host_ep in sorted(self.host_eps_by_id.iteritems(),
                                           key=lambda h: repr(h[0])):
            addrs_key = "expected_ipv%s_addrs" % self.ip_version
            if "name" in host_ep:
                # This interface has an explicit name in the data so it's
                # already resolved.
                resolved_id = combined_id.resolve(host_ep["name"])
                resolved_ifaces[resolved_id] = host_ep
            elif addrs_key in host_ep:
                # No explicit name, look for an interface with a matching IP.
                expected_ips = IPSet(host_ep[addrs_key])
                for ip, iface_names in sorted(iface_names_by_ip.iteritems()):
                    if ip in expected_ips:
                        # This endpoint matches the IP, loop over the (usually
                        # one) interface with that IP.  Sort the names to avoid
                        # non-deterministic behaviour if there are multiple
                        # conflicting matches.
                        _log.debug("Host endpoint %s matches interfaces: %s",
                                   combined_id, iface_names)
                        for iface_name in sorted(iface_names):
                            # Check for conflicting matches.
                            prev_match = iface_name_to_id.get(iface_name)
                            if prev_match == combined_id:
                                # Already matched this interface by a different
                                # IP address.
                                continue
                            elif prev_match is not None:
                                # Already matched a different interface.
                                # First match wins.
                                _log.warn(
                                    "Interface %s matched with multiple "
                                    "entries in datamodel; using %s",
                                    iface_name, prev_match)
                                continue
                            else:
                                # Else, this is the first match, record it.
                                iface_name_to_id[iface_name] = combined_id
                            # Got a match.  Since it's possible to match
                            # multiple interfaces by IP, we add the interface
                            # name into the ID to disambiguate.
                            resolved_id = combined_id.resolve(iface_name)
                            resolved_data = host_ep.copy()
                            resolved_data["name"] = iface_name
                            resolved_ifaces[resolved_id] = resolved_data
        # Fire in deletions for interfaces that no longer resolve.
        for resolved_id in self.resolved_host_eps.keys():
            if resolved_id not in resolved_ifaces:
                _log.debug("%s no longer matches", resolved_id)
                self.on_endpoint_update(resolved_id, None)
        # Fire in the updates for the new data.
        for resolved_id, data in resolved_ifaces.iteritems():
            if self.resolved_host_eps.get(resolved_id) != data:
                _log.debug("Updating data for %s", resolved_id)
                self.on_endpoint_update(resolved_id, data)
        # Update the cache so we can calculate deltas next time.
        self.resolved_host_eps = resolved_ifaces

    def _update_dirty_policy(self):
        if not self._data_model_in_sync:
            _log.debug("Datamodel not in sync, postponing update to policy")
            return
        _log.debug("Endpoints with dirty policy: %s",
                   self.endpoints_with_dirty_policy)
        while self.endpoints_with_dirty_policy:
            ep_id = self.endpoints_with_dirty_policy.pop()
            if self._is_starting_or_live(ep_id):
                self._update_tiered_policy(ep_id)

    def _update_tiered_policy(self, ep_id):
        """
        Sends an updated list of tiered policy to an endpoint.

        Recalculates the list.
        :param ep_id: ID of the endpoint to send an update to.
        """
        _log.debug("Updating policies for %s from %s", ep_id,
                   self.pol_ids_by_ep_id)
        # Order the profiles by tier and profile order, using the name of the
        # tier and profile as a tie-breaker if the orders are the same.
        profiles = []
        for pol_id in self.pol_ids_by_ep_id.iter_values(ep_id):
            try:
                tier_order = self.tier_orders[pol_id.tier]
            except KeyError:
                _log.warn(
                    "Ignoring policy %s because its tier metadata is "
                    "missing.", pol_id)
                continue
            profile_order = self.profile_orders[pol_id]
            profiles.append((tier_order, pol_id.tier, profile_order,
                             pol_id.policy_id, pol_id))
        profiles.sort()
        # Convert to an ordered dict from tier to list of profiles.
        pols_by_tier = OrderedDict()
        for _, tier, _, _, pol_id in profiles:
            pols_by_tier.setdefault(tier, []).append(pol_id)

        endpoint = self.objects_by_id[ep_id]
        endpoint.on_tiered_policy_update(pols_by_tier, async=True)

    def _on_worker_died(self, watch_greenlet):
        """
        Greenlet: spawned by the gevent Hub if our worker thread dies.
        """
        _log.critical("Worker greenlet died: %s; exiting.", watch_greenlet)
        sys.exit(1)
Beispiel #19
0
 def setUp(self):
     super(TestMultiDict, self).setUp()
     self.index = MultiDict()
Beispiel #20
0
class EndpointManager(ReferenceManager):
    def __init__(self, config, ip_type,
                 iptables_updater,
                 workload_disp_chains,
                 host_disp_chains,
                 rules_manager,
                 fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.workload_disp_chains = workload_disp_chains
        self.host_disp_chains = host_disp_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Cache of IPs applied to host endpoints.  (I.e. any interfaces that
        # aren't workload interfaces.)
        self.host_ep_ips_by_iface = {}
        # Host interface dicts by ID.  We'll resolve these with the IPs above
        # and inject the (resolved) ones as endpoints.
        self.host_eps_by_id = {}
        # Cache of interfaces that we've resolved and injected as endpoints.
        self.resolved_host_eps = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False
        self._iface_poll_greenlet = gevent.Greenlet(self._interface_poll_loop)
        self._iface_poll_greenlet.link_exception(self._on_worker_died)

    def _on_actor_started(self):
        _log.info("Endpoint manager started, spawning interface poll worker.")
        self._iface_poll_greenlet.start()

    def _create(self, combined_id):
        """
        Overrides ReferenceManager._create()
        """
        if isinstance(combined_id, WloadEndpointId):
            return WorkloadEndpoint(self.config,
                                    combined_id,
                                    self.ip_type,
                                    self.iptables_updater,
                                    self.workload_disp_chains,
                                    self.rules_mgr,
                                    self.fip_manager,
                                    self.status_reporter)
        elif isinstance(combined_id, ResolvedHostEndpointId):
            return HostEndpoint(self.config,
                                combined_id,
                                self.ip_type,
                                self.iptables_updater,
                                self.host_disp_chains,
                                self.rules_mgr,
                                self.fip_manager,
                                self.status_reporter)
        else:
            raise RuntimeError("Unknown ID type: %s" % combined_id)

    @actor_message()
    def on_tier_data_update(self, tier, data):
        """
        Message received when the metadata for a policy tier is updated
        in etcd.

        :param str tier: The name of the tier.
        :param dict|NoneType data: The dict or None, for a deletion.
        """
        _log.debug("Data for policy tier %s updated to %s", tier, data)

        # Currently, the only data we care about is the order.
        order = None if data is None else data["order"]
        if self.tier_orders.get(tier) == order:
            _log.debug("No change, ignoring")
            return

        if order is not None:
            self.tier_orders[tier] = order
        else:
            del self.tier_orders[tier]

        new_tier_sequence = sorted(self.tier_orders.iterkeys(),
                                   key=lambda k: (self.tier_orders[k], k))
        if self.tier_sequence != new_tier_sequence:
            _log.info("Sequence of profile tiers changed, refreshing all "
                      "endpoints")
            self.tier_sequence = new_tier_sequence
            self.endpoints_with_dirty_policy.update(
                self.endpoints_by_id.keys()
            )
            self._update_dirty_policy()

    @actor_message()
    def on_prof_labels_set(self, profile_id, labels):
        _log.debug("Profile labels updated for %s: %s", profile_id, labels)
        # Defer to the label index, which will call us back synchronously
        # with any match changes.
        self._label_inherit_idx.on_parent_labels_update(profile_id, labels)
        # Process any match changes that we've recorded in the callbacks.
        self._update_dirty_policy()

    @actor_message()
    def on_policy_selector_update(self, policy_id, selector_or_none,
                                  order_or_none):
        _log.debug("Policy %s selector updated to %s (%s)", policy_id,
                   selector_or_none, order_or_none)
        # Defer to the label index, which will call us back synchronously
        # via on_policy_match_started and on_policy_match_stopped.
        self.policy_index.on_expression_update(policy_id,
                                               selector_or_none)

        # Before we update the policies, check if the order has changed,
        # which would mean we need to refresh all endpoints with this policy
        # too.
        if order_or_none != self.profile_orders.get(policy_id):
            if order_or_none is not None:
                self.profile_orders[policy_id] = order_or_none
            else:
                del self.profile_orders[policy_id]
            self.endpoints_with_dirty_policy.update(
                self.policy_index.matches_by_expr_id.iter_values(policy_id)
            )

        # Finally, flush any updates to our waiting endpoints.
        self._update_dirty_policy()

    def on_policy_match_started(self, expr_id, item_id):
        """Called by the label index when a new match is started.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s now applies to endpoint %s", expr_id, item_id)
        self.pol_ids_by_ep_id.add(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def on_policy_match_stopped(self, expr_id, item_id):
        """Called by the label index when a match stops.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s no longer applies to endpoint %s",
                  expr_id, item_id)
        self.pol_ids_by_ep_id.discard(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def _on_object_started(self, endpoint_id, obj):
        """
        Callback from a LocalEndpoint to report that it has started.
        Overrides ReferenceManager._on_object_started
        """
        ep = self.endpoints_by_id.get(endpoint_id)
        obj.on_endpoint_update(ep, async=True)
        self._update_tiered_policy(endpoint_id)

    @actor_message()
    def on_datamodel_in_sync(self):
        if not self._data_model_in_sync:
            _log.info("%s: First time we've been in-sync with the datamodel,"
                      "sending snapshot to DispatchChains and FIPManager.",
                      self)
            self._data_model_in_sync = True

            # Tell the dispatch chains about the local endpoints in advance so
            # that we don't flap the dispatch chain at start-of-day.  Note:
            # the snapshot may contain information that is ahead of the
            # state that our individual LocalEndpoint actors are sending to the
            # DispatchChains actor.  That is OK!  The worst that can happen is
            # that a LocalEndpoint undoes part of our update and then goes on
            # to re-apply the update when it catches up to the snapshot.
            workload_ifaces = set()
            host_eps = set()
            for if_name, ep_id in self.endpoint_id_by_iface_name.iteritems():
                if isinstance(ep_id, WloadEndpointId):
                    workload_ifaces.add(if_name)
                else:
                    host_eps.add(if_name)
            self.workload_disp_chains.apply_snapshot(
                frozenset(workload_ifaces), async=True
            )
            self.host_disp_chains.apply_snapshot(
                frozenset(host_eps), async=True
            )
            self._update_dirty_policy()

            nat_maps = {}
            for ep_id, ep in self.endpoints_by_id.iteritems():
                if ep_id in self.local_endpoint_ids:
                    nat_map = ep.get(nat_key(self.ip_type), None)
                    if nat_map:
                        nat_maps[ep_id] = nat_map
            self.fip_manager.apply_snapshot(nat_maps, async=True)

    @actor_message()
    def on_host_ep_update(self, combined_id, data):
        if data is not None:
            self.host_eps_by_id[combined_id] = data
        else:
            self.host_eps_by_id.pop(combined_id, None)
        self._resolve_host_eps()

    @actor_message()
    def on_endpoint_update(self, endpoint_id, endpoint, force_reprogram=False):
        """
        Event to indicate that an endpoint has been updated (including
        creation or deletion).

        :param EndpointId endpoint_id: The endpoint ID in question.
        :param dict[str]|NoneType endpoint: Dictionary of all endpoint
            data or None if the endpoint is to be deleted.
        """
        if endpoint_id.host != self.config.HOSTNAME:
            _log.debug("Skipping endpoint %s; not on our host.", endpoint_id)
            return

        if self._is_starting_or_live(endpoint_id):
            # Local endpoint thread is running; tell it of the change.
            _log.info("Update for live endpoint %s", endpoint_id)
            self.objects_by_id[endpoint_id].on_endpoint_update(
                endpoint, force_reprogram=force_reprogram, async=True)

        old_ep = self.endpoints_by_id.pop(endpoint_id, {})
        # Interface name shouldn't change but popping it now is correct for
        # deletes and we add it back in below on create/modify.
        old_iface_name = old_ep.get("name")
        self.endpoint_id_by_iface_name.pop(old_iface_name, None)
        if endpoint is None:
            # Deletion. Remove from the list.
            _log.info("Endpoint %s deleted", endpoint_id)
            if endpoint_id in self.local_endpoint_ids:
                self.decref(endpoint_id)
                self.local_endpoint_ids.remove(endpoint_id)
                self._label_inherit_idx.on_item_update(endpoint_id, None, None)
                assert endpoint_id not in self.pol_ids_by_ep_id
        else:
            # Creation or modification
            _log.info("Endpoint %s modified or created", endpoint_id)
            self.endpoints_by_id[endpoint_id] = endpoint
            self.endpoint_id_by_iface_name[endpoint["name"]] = endpoint_id
            if endpoint_id not in self.local_endpoint_ids:
                # This will trigger _on_object_activated to pass the endpoint
                # we just saved off to the endpoint.
                _log.debug("Endpoint wasn't known before, increffing it")
                self.local_endpoint_ids.add(endpoint_id)
                self.get_and_incref(endpoint_id)
            self._label_inherit_idx.on_item_update(
                endpoint_id,
                endpoint.get("labels", {}),
                endpoint.get("profile_ids", [])
            )

        self._update_dirty_policy()

    @actor_message()
    def on_interface_update(self, name, iface_up):
        """
        Called when an interface is created or changes state.

        The interface may be any interface on the host, not necessarily
        one managed by any endpoint of this server.
        """
        try:
            endpoint_id = self.endpoint_id_by_iface_name[name]
        except KeyError:
            _log.debug("Update on interface %s that we do not care about",
                       name)
        else:
            _log.info("Endpoint %s received interface update for %s",
                      endpoint_id, name)
            if self._is_starting_or_live(endpoint_id):
                # LocalEndpoint is running, so tell it about the change.
                ep = self.objects_by_id[endpoint_id]
                ep.on_interface_update(iface_up, async=True)

    def _interface_poll_loop(self):
        """Greenlet: Polls host endpoints for changes to their IP addresses.

        Sends updates to the EndpointManager via the _on_iface_ips_update()
        message.

        If polling is disabled, then it reads the interfaces once and then
        stops.
        """
        known_interfaces = {}
        while True:
            known_interfaces = self._poll_interfaces(known_interfaces)
            if self.config.HOST_IF_POLL_INTERVAL_SECS <= 0:
                _log.info("Host interface polling disabled, stopping after "
                          "initial read. Further changes to host endpoint "
                          "IPs will be ignored.")
                break
            gevent.sleep(self.config.HOST_IF_POLL_INTERVAL_SECS)

    def _poll_interfaces(self, known_interfaces):
        """Does a single poll of the host interfaces, looking for IP changes.

        Sends updates to the EndpointManager via the _on_iface_ips_update()
        message.

        This is broken out form the loop above to make it easier to test.

        :param known_interfaces:
        :return:
        """
        # We only care about host interfaces, not workload ones.
        exclude_prefix = self.config.IFACE_PREFIX
        # Get the IPs for each interface.
        ips_by_iface = devices.list_ips_by_iface(self.ip_type)
        for iface, ips in ips_by_iface.items():
            if iface.startswith(exclude_prefix):
                # Ignore non-host interfaces.
                ips_by_iface.pop(iface)
            else:
                # Compare with the set of IPs that were there before.
                # We pop interfaces that we see so that we can clean up
                # deletions below.
                old_ips = known_interfaces.pop(iface, None)
                if old_ips != ips:
                    _log.debug("IPs of interface %s changed to %s",
                               iface, ips)
                    self._on_iface_ips_update(iface, ips, async=True)
        # Clean up deletions.  Anything left in known_interfaces has
        # been deleted.
        for iface, ips in known_interfaces.iteritems():
            self._on_iface_ips_update(iface, None, async=True)
        # Update our cache of known interfaces for the next loop.
        return ips_by_iface

    @actor_message()
    def _on_iface_ips_update(self, iface_name, ip_addrs):
        """Message sent by _poll_interface_ips when it detects a change.

        :param iface_name: Name of the interface that has been updated.
        :param ip_addrs: set of IP addresses, or None if the interface no
               longer exists (or has no IPs).
        """
        _log.info("Interface %s now has IPs %s", iface_name, ip_addrs)
        if ip_addrs is not None:
            self.host_ep_ips_by_iface[iface_name] = ip_addrs
        else:
            self.host_ep_ips_by_iface.pop(iface_name, None)
        # Since changes to IPs can change which host endpoint objects apply to
        # which interfaces, we need to resolve IPs and host endpoints.
        self._resolve_host_eps()

    def _resolve_host_eps(self):
        """Resolves the host endpoint data we've learned from etcd with
        IP addresses and interface names learned from the kernel.

        Host interfaces that have matching IPs get combined with interface
        name learned from the kernel and updated via on_endpoint_update().

        In the case where multiple interfaces have the same IP address,
        a copy of the host endpoint will be resolved with each interface.
        """
        # Invert the interface name to IP mapping to allow us to do an IP to
        # interface name lookup.
        iface_names_by_ip = defaultdict(set)
        for iface, ips in self.host_ep_ips_by_iface.iteritems():
            for ip in ips:
                iface_names_by_ip[ip].add(iface)
        # Iterate over the host endpoints, looking for corresponding IPs.
        resolved_ifaces = {}
        iface_name_to_id = {}
        # For repeatability, we sort the endpoint data.  We don't care what
        # the sort order is, only that it's stable so we just use the repr()
        # of the ID.
        for combined_id, host_ep in sorted(self.host_eps_by_id.iteritems(),
                                           key=lambda h: repr(h[0])):
            addrs_key = "expected_ipv%s_addrs" % self.ip_version
            if "name" in host_ep:
                # This interface has an explicit name in the data so it's
                # already resolved.
                resolved_id = combined_id.resolve(host_ep["name"])
                resolved_ifaces[resolved_id] = host_ep
            elif addrs_key in host_ep:
                # No explicit name, look for an interface with a matching IP.
                expected_ips = IPSet(host_ep[addrs_key])
                for ip, iface_names in sorted(iface_names_by_ip.iteritems()):
                    if ip in expected_ips:
                        # This endpoint matches the IP, loop over the (usually
                        # one) interface with that IP.  Sort the names to avoid
                        # non-deterministic behaviour if there are multiple
                        # conflicting matches.
                        _log.debug("Host endpoint %s matches interfaces: %s",
                                   combined_id, iface_names)
                        for iface_name in sorted(iface_names):
                            # Check for conflicting matches.
                            prev_match = iface_name_to_id.get(iface_name)
                            if prev_match == combined_id:
                                # Already matched this interface by a different
                                # IP address.
                                continue
                            elif prev_match is not None:
                                # Already matched a different interface.
                                # First match wins.
                                _log.warn("Interface %s matched with multiple "
                                          "entries in datamodel; using %s",
                                          iface_name, prev_match)
                                continue
                            else:
                                # Else, this is the first match, record it.
                                iface_name_to_id[iface_name] = combined_id
                            # Got a match.  Since it's possible to match
                            # multiple interfaces by IP, we add the interface
                            # name into the ID to disambiguate.
                            resolved_id = combined_id.resolve(iface_name)
                            resolved_data = host_ep.copy()
                            resolved_data["name"] = iface_name
                            resolved_ifaces[resolved_id] = resolved_data
        # Fire in deletions for interfaces that no longer resolve.
        for resolved_id in self.resolved_host_eps.keys():
            if resolved_id not in resolved_ifaces:
                _log.debug("%s no longer matches", resolved_id)
                self.on_endpoint_update(resolved_id, None)
        # Fire in the updates for the new data.
        for resolved_id, data in resolved_ifaces.iteritems():
            if self.resolved_host_eps.get(resolved_id) != data:
                _log.debug("Updating data for %s", resolved_id)
                self.on_endpoint_update(resolved_id, data)
        # Update the cache so we can calculate deltas next time.
        self.resolved_host_eps = resolved_ifaces

    def _update_dirty_policy(self):
        if not self._data_model_in_sync:
            _log.debug("Datamodel not in sync, postponing update to policy")
            return
        _log.debug("Endpoints with dirty policy: %s",
                   self.endpoints_with_dirty_policy)
        while self.endpoints_with_dirty_policy:
            ep_id = self.endpoints_with_dirty_policy.pop()
            if self._is_starting_or_live(ep_id):
                self._update_tiered_policy(ep_id)

    def _update_tiered_policy(self, ep_id):
        """
        Sends an updated list of tiered policy to an endpoint.

        Recalculates the list.
        :param ep_id: ID of the endpoint to send an update to.
        """
        _log.debug("Updating policies for %s from %s", ep_id,
                   self.pol_ids_by_ep_id)
        # Order the profiles by tier and profile order, using the name of the
        # tier and profile as a tie-breaker if the orders are the same.
        profiles = []
        for pol_id in self.pol_ids_by_ep_id.iter_values(ep_id):
            try:
                tier_order = self.tier_orders[pol_id.tier]
            except KeyError:
                _log.warn("Ignoring profile %s because its tier metadata is "
                          "missing.")
                continue
            profile_order = self.profile_orders[pol_id]
            profiles.append((tier_order, pol_id.tier,
                             profile_order, pol_id.policy_id,
                             pol_id))
        profiles.sort()
        # Convert to an ordered dict from tier to list of profiles.
        pols_by_tier = OrderedDict()
        for _, tier, _, _, pol_id in profiles:
            pols_by_tier.setdefault(tier, []).append(pol_id)

        endpoint = self.objects_by_id[ep_id]
        endpoint.on_tiered_policy_update(pols_by_tier, async=True)

    def _on_worker_died(self, watch_greenlet):
        """
        Greenlet: spawned by the gevent Hub if our worker thread dies.
        """
        _log.critical("Worker greenlet died: %s; exiting.", watch_greenlet)
        sys.exit(1)
Beispiel #21
0
class EndpointManager(ReferenceManager):
    def __init__(self, config, ip_type,
                 iptables_updater,
                 dispatch_chains,
                 rules_manager,
                 fip_manager,
                 status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.dispatch_chains = dispatch_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False

    def _create(self, combined_id):
        """
        Overrides ReferenceManager._create()
        """
        return LocalEndpoint(self.config,
                             combined_id,
                             self.ip_type,
                             self.iptables_updater,
                             self.dispatch_chains,
                             self.rules_mgr,
                             self.fip_manager,
                             self.status_reporter)

    @actor_message()
    def on_tier_data_update(self, tier, data):
        """
        Message received when the metadata for a policy tier is updated
        in etcd.

        :param str tier: The name of the tier.
        :param dict|NoneType data: The dict or None, for a deletion.
        """
        _log.debug("Data for policy tier %s updated to %s", tier, data)

        # Currently, the only data we care about is the order.
        order = None if data is None else data["order"]
        if self.tier_orders.get(tier) == order:
            _log.debug("No change, ignoring")
            return

        if order is not None:
            self.tier_orders[tier] = order
        else:
            del self.tier_orders[tier]

        new_tier_sequence = sorted(self.tier_orders.iterkeys(),
                                   key=lambda k: (self.tier_orders[k], k))
        if self.tier_sequence != new_tier_sequence:
            _log.info("Sequence of profile tiers changed, refreshing all "
                      "endpoints")
            self.tier_sequence = new_tier_sequence
            self.endpoints_with_dirty_policy.update(
                self.endpoints_by_id.keys()
            )
            self._update_dirty_policy()

    @actor_message()
    def on_prof_labels_set(self, profile_id, labels):
        _log.debug("Profile labels updated for %s: %s", profile_id, labels)
        # Defer to the label index, which will call us back synchronously
        # with any match changes.
        self._label_inherit_idx.on_parent_labels_update(profile_id, labels)
        # Process any match changes that we've recorded in the callbacks.
        self._update_dirty_policy()

    @actor_message()
    def on_policy_selector_update(self, policy_id, selector_or_none,
                                  order_or_none):
        _log.debug("Policy %s selector updated to %s (%s)", policy_id,
                   selector_or_none, order_or_none)
        # Defer to the label index, which will call us back synchronously
        # via on_policy_match_started and on_policy_match_stopped.
        self.policy_index.on_expression_update(policy_id,
                                               selector_or_none)

        # Before we update the policies, check if the order has changed,
        # which would mean we need to refresh all endpoints with this policy
        # too.
        if order_or_none != self.profile_orders.get(policy_id):
            if order_or_none is not None:
                self.profile_orders[policy_id] = order_or_none
            else:
                del self.profile_orders[policy_id]
            self.endpoints_with_dirty_policy.update(
                self.policy_index.matches_by_expr_id.iter_values(policy_id)
            )

        # Finally, flush any updates to our waiting endpoints.
        self._update_dirty_policy()

    def on_policy_match_started(self, expr_id, item_id):
        """Called by the label index when a new match is started.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s now applies to endpoint %s", expr_id, item_id)
        self.pol_ids_by_ep_id.add(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def on_policy_match_stopped(self, expr_id, item_id):
        """Called by the label index when a match stops.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s no longer applies to endpoint %s",
                  expr_id, item_id)
        self.pol_ids_by_ep_id.discard(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def _on_object_started(self, endpoint_id, obj):
        """
        Callback from a LocalEndpoint to report that it has started.
        Overrides ReferenceManager._on_object_started
        """
        ep = self.endpoints_by_id.get(endpoint_id)
        obj.on_endpoint_update(ep, async=True)
        self._update_tiered_policy(endpoint_id)

    @actor_message()
    def on_datamodel_in_sync(self):
        if not self._data_model_in_sync:
            _log.info("%s: First time we've been in-sync with the datamodel,"
                      "sending snapshot to DispatchChains and FIPManager.",
                      self)
            self._data_model_in_sync = True

            # Tell the dispatch chains about the local endpoints in advance so
            # that we don't flap the dispatch chain at start-of-day.  Note:
            # the snapshot may contain information that is ahead of the
            # state that our individual LocalEndpoint actors are sending to the
            # DispatchChains actor.  That is OK!  The worst that can happen is
            # that a LocalEndpoint undoes part of our update and then goes on
            # to re-apply the update when it catches up to the snapshot.
            local_ifaces = frozenset(self.endpoint_id_by_iface_name.keys())
            self.dispatch_chains.apply_snapshot(local_ifaces, async=True)
            self._update_dirty_policy()

            nat_maps = {}
            for ep_id, ep in self.endpoints_by_id.iteritems():
                if ep_id in self.local_endpoint_ids:
                    nat_map = ep.get(nat_key(self.ip_type), None)
                    if nat_map:
                        nat_maps[ep_id] = nat_map
            self.fip_manager.apply_snapshot(nat_maps, async=True)

    @actor_message()
    def on_endpoint_update(self, endpoint_id, endpoint, force_reprogram=False):
        """
        Event to indicate that an endpoint has been updated (including
        creation or deletion).

        :param EndpointId endpoint_id: The endpoint ID in question.
        :param dict[str]|NoneType endpoint: Dictionary of all endpoint
            data or None if the endpoint is to be deleted.
        """
        if endpoint_id.host != self.config.HOSTNAME:
            _log.debug("Skipping endpoint %s; not on our host.", endpoint_id)
            return

        if self._is_starting_or_live(endpoint_id):
            # Local endpoint thread is running; tell it of the change.
            _log.info("Update for live endpoint %s", endpoint_id)
            self.objects_by_id[endpoint_id].on_endpoint_update(
                endpoint, force_reprogram=force_reprogram, async=True)

        old_ep = self.endpoints_by_id.pop(endpoint_id, {})
        # Interface name shouldn't change but popping it now is correct for
        # deletes and we add it back in below on create/modify.
        old_iface_name = old_ep.get("name")
        self.endpoint_id_by_iface_name.pop(old_iface_name, None)
        if endpoint is None:
            # Deletion. Remove from the list.
            _log.info("Endpoint %s deleted", endpoint_id)
            if endpoint_id in self.local_endpoint_ids:
                self.decref(endpoint_id)
                self.local_endpoint_ids.remove(endpoint_id)
                self._label_inherit_idx.on_item_update(endpoint_id, None, None)
                assert endpoint_id not in self.pol_ids_by_ep_id
        else:
            # Creation or modification
            _log.info("Endpoint %s modified or created", endpoint_id)
            self.endpoints_by_id[endpoint_id] = endpoint
            self.endpoint_id_by_iface_name[endpoint["name"]] = endpoint_id
            if endpoint_id not in self.local_endpoint_ids:
                # This will trigger _on_object_activated to pass the endpoint
                # we just saved off to the endpoint.
                _log.debug("Endpoint wasn't known before, increffing it")
                self.local_endpoint_ids.add(endpoint_id)
                self.get_and_incref(endpoint_id)
            self._label_inherit_idx.on_item_update(
                endpoint_id,
                endpoint.get("labels", {}),
                endpoint.get("profile_ids", [])
            )

        self._update_dirty_policy()

    @actor_message()
    def on_interface_update(self, name, iface_up):
        """
        Called when an interface is created or changes state.

        The interface may be any interface on the host, not necessarily
        one managed by any endpoint of this server.
        """
        try:
            endpoint_id = self.endpoint_id_by_iface_name[name]
        except KeyError:
            _log.debug("Update on interface %s that we do not care about",
                       name)
        else:
            _log.info("Endpoint %s received interface update for %s",
                      endpoint_id, name)
            if self._is_starting_or_live(endpoint_id):
                # LocalEndpoint is running, so tell it about the change.
                ep = self.objects_by_id[endpoint_id]
                ep.on_interface_update(iface_up, async=True)

    def _update_dirty_policy(self):
        if not self._data_model_in_sync:
            _log.debug("Datamodel not in sync, postponing update to policy")
            return
        _log.debug("Endpoints with dirty policy: %s",
                   self.endpoints_with_dirty_policy)
        while self.endpoints_with_dirty_policy:
            ep_id = self.endpoints_with_dirty_policy.pop()
            if self._is_starting_or_live(ep_id):
                self._update_tiered_policy(ep_id)

    def _update_tiered_policy(self, ep_id):
        """
        Sends an updated list of tiered policy to an endpoint.

        Recalculates the list.
        :param ep_id: ID of the endpoint to send an update to.
        """
        _log.debug("Updating policies for %s from %s", ep_id,
                   self.pol_ids_by_ep_id)
        # Order the profiles by tier and profile order, using the name of the
        # tier and profile as a tie-breaker if the orders are the same.
        profiles = []
        for pol_id in self.pol_ids_by_ep_id.iter_values(ep_id):
            try:
                tier_order = self.tier_orders[pol_id.tier]
            except KeyError:
                _log.warn("Ignoring profile %s because its tier metadata is "
                          "missing.")
                continue
            profile_order = self.profile_orders[pol_id]
            profiles.append((tier_order, pol_id.tier,
                             profile_order, pol_id.policy_id,
                             pol_id))
        profiles.sort()
        # Convert to an ordered dict from tier to list of profiles.
        pols_by_tier = OrderedDict()
        for _, tier, _, _, pol_id in profiles:
            pols_by_tier.setdefault(tier, []).append(pol_id)

        endpoint = self.objects_by_id[ep_id]
        endpoint.on_tiered_policy_update(pols_by_tier, async=True)
Beispiel #22
0
class EndpointManager(ReferenceManager):
    def __init__(self, config, ip_type, iptables_updater, dispatch_chains,
                 rules_manager, fip_manager, status_reporter):
        super(EndpointManager, self).__init__(qualifier=ip_type)

        # Configuration and version to use
        self.config = config
        self.ip_type = ip_type
        self.ip_version = futils.IP_TYPE_TO_VERSION[ip_type]

        # Peers/utility classes.
        self.iptables_updater = iptables_updater
        self.dispatch_chains = dispatch_chains
        self.rules_mgr = rules_manager
        self.status_reporter = status_reporter
        self.fip_manager = fip_manager

        # All endpoint dicts that are on this host.
        self.endpoints_by_id = {}
        # Dict that maps from interface name ("tap1234") to endpoint ID.
        self.endpoint_id_by_iface_name = {}

        # Set of endpoints that are live on this host.  I.e. ones that we've
        # increffed.
        self.local_endpoint_ids = set()

        # Index tracking what policy applies to what endpoints.
        self.policy_index = LabelValueIndex()
        self.policy_index.on_match_started = self.on_policy_match_started
        self.policy_index.on_match_stopped = self.on_policy_match_stopped
        self._label_inherit_idx = LabelInheritanceIndex(self.policy_index)
        # Tier orders by tier ID.  We use this to look up the order when we're
        # sorting the tiers.
        self.tier_orders = {}
        # Cache of the current ordering of tier IDs.
        self.tier_sequence = []
        # And their associated orders.
        self.profile_orders = {}
        # Set of profile IDs to apply to each endpoint ID.
        self.pol_ids_by_ep_id = MultiDict()
        self.endpoints_with_dirty_policy = set()

        self._data_model_in_sync = False

    def _create(self, combined_id):
        """
        Overrides ReferenceManager._create()
        """
        return LocalEndpoint(self.config, combined_id, self.ip_type,
                             self.iptables_updater, self.dispatch_chains,
                             self.rules_mgr, self.fip_manager,
                             self.status_reporter)

    @actor_message()
    def on_tier_data_update(self, tier, data):
        """
        Message received when the metadata for a policy tier is updated
        in etcd.

        :param str tier: The name of the tier.
        :param dict|NoneType data: The dict or None, for a deletion.
        """
        _log.debug("Data for policy tier %s updated to %s", tier, data)

        # Currently, the only data we care about is the order.
        order = None if data is None else data["order"]
        if self.tier_orders.get(tier) == order:
            _log.debug("No change, ignoring")
            return

        if order is not None:
            self.tier_orders[tier] = order
        else:
            del self.tier_orders[tier]

        new_tier_sequence = sorted(self.tier_orders.iterkeys(),
                                   key=lambda k: (self.tier_orders[k], k))
        if self.tier_sequence != new_tier_sequence:
            _log.info("Sequence of profile tiers changed, refreshing all "
                      "endpoints")
            self.tier_sequence = new_tier_sequence
            self.endpoints_with_dirty_policy.update(
                self.endpoints_by_id.keys())
            self._update_dirty_policy()

    @actor_message()
    def on_prof_labels_set(self, profile_id, labels):
        _log.debug("Profile labels updated for %s: %s", profile_id, labels)
        # Defer to the label index, which will call us back synchronously
        # with any match changes.
        self._label_inherit_idx.on_parent_labels_update(profile_id, labels)
        # Process any match changes that we've recorded in the callbacks.
        self._update_dirty_policy()

    @actor_message()
    def on_policy_selector_update(self, policy_id, selector_or_none,
                                  order_or_none):
        _log.debug("Policy %s selector updated to %s (%s)", policy_id,
                   selector_or_none, order_or_none)
        # Defer to the label index, which will call us back synchronously
        # via on_policy_match_started and on_policy_match_stopped.
        self.policy_index.on_expression_update(policy_id, selector_or_none)

        # Before we update the policies, check if the order has changed,
        # which would mean we need to refresh all endpoints with this policy
        # too.
        if order_or_none != self.profile_orders.get(policy_id):
            if order_or_none is not None:
                self.profile_orders[policy_id] = order_or_none
            else:
                del self.profile_orders[policy_id]
            self.endpoints_with_dirty_policy.update(
                self.policy_index.matches_by_expr_id.iter_values(policy_id))

        # Finally, flush any updates to our waiting endpoints.
        self._update_dirty_policy()

    def on_policy_match_started(self, expr_id, item_id):
        """Called by the label index when a new match is started.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s now applies to endpoint %s", expr_id, item_id)
        self.pol_ids_by_ep_id.add(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def on_policy_match_stopped(self, expr_id, item_id):
        """Called by the label index when a match stops.

        Records the update but processing is deferred to
        the next call to self._update_dirty_policy().
        """
        _log.info("Policy %s no longer applies to endpoint %s", expr_id,
                  item_id)
        self.pol_ids_by_ep_id.discard(item_id, expr_id)
        self.endpoints_with_dirty_policy.add(item_id)

    def _on_object_started(self, endpoint_id, obj):
        """
        Callback from a LocalEndpoint to report that it has started.
        Overrides ReferenceManager._on_object_started
        """
        ep = self.endpoints_by_id.get(endpoint_id)
        obj.on_endpoint_update(ep, async=True)
        self._update_tiered_policy(endpoint_id)

    @actor_message()
    def on_datamodel_in_sync(self):
        if not self._data_model_in_sync:
            _log.info(
                "%s: First time we've been in-sync with the datamodel,"
                "sending snapshot to DispatchChains and FIPManager.", self)
            self._data_model_in_sync = True

            # Tell the dispatch chains about the local endpoints in advance so
            # that we don't flap the dispatch chain at start-of-day.  Note:
            # the snapshot may contain information that is ahead of the
            # state that our individual LocalEndpoint actors are sending to the
            # DispatchChains actor.  That is OK!  The worst that can happen is
            # that a LocalEndpoint undoes part of our update and then goes on
            # to re-apply the update when it catches up to the snapshot.
            local_ifaces = frozenset(self.endpoint_id_by_iface_name.keys())
            self.dispatch_chains.apply_snapshot(local_ifaces, async=True)
            self._update_dirty_policy()

            nat_maps = {}
            for ep_id, ep in self.endpoints_by_id.iteritems():
                if ep_id in self.local_endpoint_ids:
                    nat_map = ep.get(nat_key(self.ip_type), None)
                    if nat_map:
                        nat_maps[ep_id] = nat_map
            self.fip_manager.apply_snapshot(nat_maps, async=True)

    @actor_message()
    def on_endpoint_update(self, endpoint_id, endpoint, force_reprogram=False):
        """
        Event to indicate that an endpoint has been updated (including
        creation or deletion).

        :param EndpointId endpoint_id: The endpoint ID in question.
        :param dict[str]|NoneType endpoint: Dictionary of all endpoint
            data or None if the endpoint is to be deleted.
        """
        if endpoint_id.host != self.config.HOSTNAME:
            _log.debug("Skipping endpoint %s; not on our host.", endpoint_id)
            return

        if self._is_starting_or_live(endpoint_id):
            # Local endpoint thread is running; tell it of the change.
            _log.info("Update for live endpoint %s", endpoint_id)
            self.objects_by_id[endpoint_id].on_endpoint_update(
                endpoint, force_reprogram=force_reprogram, async=True)

        old_ep = self.endpoints_by_id.pop(endpoint_id, {})
        # Interface name shouldn't change but popping it now is correct for
        # deletes and we add it back in below on create/modify.
        old_iface_name = old_ep.get("name")
        self.endpoint_id_by_iface_name.pop(old_iface_name, None)
        if endpoint is None:
            # Deletion. Remove from the list.
            _log.info("Endpoint %s deleted", endpoint_id)
            if endpoint_id in self.local_endpoint_ids:
                self.decref(endpoint_id)
                self.local_endpoint_ids.remove(endpoint_id)
                self._label_inherit_idx.on_item_update(endpoint_id, None, None)
                assert endpoint_id not in self.pol_ids_by_ep_id
        else:
            # Creation or modification
            _log.info("Endpoint %s modified or created", endpoint_id)
            self.endpoints_by_id[endpoint_id] = endpoint
            self.endpoint_id_by_iface_name[endpoint["name"]] = endpoint_id
            if endpoint_id not in self.local_endpoint_ids:
                # This will trigger _on_object_activated to pass the endpoint
                # we just saved off to the endpoint.
                _log.debug("Endpoint wasn't known before, increffing it")
                self.local_endpoint_ids.add(endpoint_id)
                self.get_and_incref(endpoint_id)
            self._label_inherit_idx.on_item_update(
                endpoint_id, endpoint.get("labels", {}),
                endpoint.get("profile_ids", []))

        self._update_dirty_policy()

    @actor_message()
    def on_interface_update(self, name, iface_up):
        """
        Called when an interface is created or changes state.

        The interface may be any interface on the host, not necessarily
        one managed by any endpoint of this server.
        """
        try:
            endpoint_id = self.endpoint_id_by_iface_name[name]
        except KeyError:
            _log.debug("Update on interface %s that we do not care about",
                       name)
        else:
            _log.info("Endpoint %s received interface update for %s",
                      endpoint_id, name)
            if self._is_starting_or_live(endpoint_id):
                # LocalEndpoint is running, so tell it about the change.
                ep = self.objects_by_id[endpoint_id]
                ep.on_interface_update(iface_up, async=True)

    def _update_dirty_policy(self):
        if not self._data_model_in_sync:
            _log.debug("Datamodel not in sync, postponing update to policy")
            return
        _log.debug("Endpoints with dirty policy: %s",
                   self.endpoints_with_dirty_policy)
        while self.endpoints_with_dirty_policy:
            ep_id = self.endpoints_with_dirty_policy.pop()
            if self._is_starting_or_live(ep_id):
                self._update_tiered_policy(ep_id)

    def _update_tiered_policy(self, ep_id):
        """
        Sends an updated list of tiered policy to an endpoint.

        Recalculates the list.
        :param ep_id: ID of the endpoint to send an update to.
        """
        _log.debug("Updating policies for %s from %s", ep_id,
                   self.pol_ids_by_ep_id)
        # Order the profiles by tier and profile order, using the name of the
        # tier and profile as a tie-breaker if the orders are the same.
        profiles = []
        for pol_id in self.pol_ids_by_ep_id.iter_values(ep_id):
            try:
                tier_order = self.tier_orders[pol_id.tier]
            except KeyError:
                _log.warn("Ignoring profile %s because its tier metadata is "
                          "missing.")
                continue
            profile_order = self.profile_orders[pol_id]
            profiles.append((tier_order, pol_id.tier, profile_order,
                             pol_id.policy_id, pol_id))
        profiles.sort()
        # Convert to an ordered dict from tier to list of profiles.
        pols_by_tier = OrderedDict()
        for _, tier, _, _, pol_id in profiles:
            pols_by_tier.setdefault(tier, []).append(pol_id)

        endpoint = self.objects_by_id[ep_id]
        endpoint.on_tiered_policy_update(pols_by_tier, async=True)
Beispiel #23
0
class TestMultiDict(TestCase):
    def setUp(self):
        super(TestMultiDict, self).setUp()
        self.index = MultiDict()

    def test_add_single(self):
        self.index.add("k", "v")
        self.assertTrue(self.index.contains("k", "v"))
        self.assertEqual(set(self.index.iter_values("k")), set(["v"]))

    def test_add_remove_single(self):
        self.index.add("k", "v")
        self.index.discard("k", "v")
        self.assertFalse(self.index.contains("k", "v"))
        self.assertEqual(self.index._index, {})

    def test_empty(self):
        self.assertFalse(bool(self.index))
        self.assertEqual(self.index.num_items("k"), 0)
        self.assertEqual(list(self.index.iter_values("k")), [])

    def test_add_multiple(self):
        self.index.add("k", "v")
        self.assertTrue(bool(self.index))
        self.assertEqual(self.index.num_items("k"), 1)
        self.index.add("k", "v")
        self.assertEqual(self.index.num_items("k"), 1)
        self.index.add("k", "v2")
        self.assertEqual(self.index.num_items("k"), 2)
        self.index.add("k", "v3")
        self.assertEqual(self.index.num_items("k"), 3)
        self.assertIn("k", self.index)
        self.assertNotIn("k2", self.index)
        self.assertTrue(self.index.contains("k", "v"))
        self.assertTrue(self.index.contains("k", "v2"))
        self.assertTrue(self.index.contains("k", "v3"))
        self.assertEqual(self.index._index, {"k": set(["v", "v2", "v3"])})
        self.assertEqual(set(self.index.iter_values("k")),
                         set(["v", "v2", "v3"]))
        self.index.discard("k", "v")
        self.index.discard("k", "v2")
        self.assertTrue(self.index.contains("k", "v3"))
        self.index.discard("k", "v3")
        self.assertEqual(self.index._index, {})
Beispiel #24
0
class TestMultiDict(TestCase):
    def setUp(self):
        super(TestMultiDict, self).setUp()
        self.index = MultiDict()

    def test_add_single(self):
        self.index.add("k", "v")
        self.assertTrue(self.index.contains("k", "v"))
        self.assertEqual(set(self.index.iter_values("k")),
                         set(["v"]))

    def test_add_remove_single(self):
        self.index.add("k", "v")
        self.index.discard("k", "v")
        self.assertFalse(self.index.contains("k", "v"))
        self.assertEqual(self.index._index, {})

    def test_empty(self):
        self.assertFalse(bool(self.index))
        self.assertEqual(self.index.num_items("k"), 0)
        self.assertEqual(list(self.index.iter_values("k")), [])

    def test_add_multiple(self):
        self.index.add("k", "v")
        self.assertTrue(bool(self.index))
        self.assertEqual(self.index.num_items("k"), 1)
        self.index.add("k", "v")
        self.assertEqual(self.index.num_items("k"), 1)
        self.index.add("k", "v2")
        self.assertEqual(self.index.num_items("k"), 2)
        self.index.add("k", "v3")
        self.assertEqual(self.index.num_items("k"), 3)
        self.assertIn("k", self.index)
        self.assertNotIn("k2", self.index)
        self.assertTrue(self.index.contains("k", "v"))
        self.assertTrue(self.index.contains("k", "v2"))
        self.assertTrue(self.index.contains("k", "v3"))
        self.assertEqual(self.index._index, {"k": set(["v", "v2", "v3"])})
        self.assertEqual(set(self.index.iter_values("k")),
                         set(["v", "v2", "v3"]))
        self.index.discard("k", "v")
        self.index.discard("k", "v2")
        self.assertTrue(self.index.contains("k", "v3"))
        self.index.discard("k", "v3")
        self.assertEqual(self.index._index, {})