407 lines
15 KiB
Python
407 lines
15 KiB
Python
# Copyright (c) 2015 VMware, Inc. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
|
|
from __future__ import print_function
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
|
|
from oslo_log import log as logging
|
|
|
|
from congress.datalog import base
|
|
from congress.datalog import compile
|
|
from congress.datalog import ruleset
|
|
from congress.datalog import topdown
|
|
from congress.datalog import utility
|
|
from congress import exception
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class RuleHandlingMixin(object):
|
|
|
|
# External Interface
|
|
|
|
def initialize_tables(self, tablenames, facts):
|
|
"""Event handler for (re)initializing a collection of tables
|
|
|
|
@facts must be an iterable containing compile.Fact objects.
|
|
"""
|
|
LOG.info("initialize_tables")
|
|
cleared_tables = set(tablenames)
|
|
for t in tablenames:
|
|
self.rules.clear_table(t)
|
|
|
|
count = 0
|
|
extra_tables = set()
|
|
ignored_facts = 0
|
|
for f in facts:
|
|
if f.table not in cleared_tables:
|
|
extra_tables.add(f.table)
|
|
ignored_facts += 1
|
|
else:
|
|
self.rules.add_rule(f.table, f)
|
|
count += 1
|
|
if self.schema:
|
|
self.schema.update(f, True)
|
|
if ignored_facts > 0:
|
|
LOG.error("initialize_tables ignored %d facts for tables "
|
|
"%s not included in the list of tablenames %s",
|
|
ignored_facts, extra_tables, cleared_tables)
|
|
LOG.info("initialized %d tables with %d facts",
|
|
len(cleared_tables), count)
|
|
|
|
def insert(self, rule):
|
|
changes = self.update([compile.Event(formula=rule, insert=True)])
|
|
return [event.formula for event in changes]
|
|
|
|
def delete(self, rule):
|
|
changes = self.update([compile.Event(formula=rule, insert=False)])
|
|
return [event.formula for event in changes]
|
|
|
|
def _update_lit_schema(self, lit, is_insert):
|
|
if self.schema is None:
|
|
raise exception.PolicyException(
|
|
"Cannot update schema because theory %s doesn't have "
|
|
"schema." % self.name)
|
|
|
|
if self.schema.complete:
|
|
# complete means the schema is pre-built and shouldn't be updated
|
|
return None
|
|
return self.schema.update(lit, is_insert)
|
|
|
|
def update_rule_schema(self, rule, is_insert):
|
|
schema_changes = []
|
|
if self.schema is None or not self.theories or self.schema.complete:
|
|
# complete means the schema is pre-built like datasoures'
|
|
return schema_changes
|
|
|
|
if isinstance(rule, compile.Fact) or isinstance(rule, compile.Literal):
|
|
schema_changes.append(self._update_lit_schema(rule, is_insert))
|
|
return schema_changes
|
|
|
|
schema_changes.append(self._update_lit_schema(rule.head, is_insert))
|
|
|
|
for lit in rule.body:
|
|
if lit.is_builtin():
|
|
continue
|
|
active_theory = lit.table.service or self.name
|
|
if active_theory not in self.theories:
|
|
continue
|
|
schema_changes.append(
|
|
self.theories[active_theory]._update_lit_schema(lit,
|
|
is_insert))
|
|
|
|
return schema_changes
|
|
|
|
def revert_schema(self, schema_changes):
|
|
if not self.theories:
|
|
return
|
|
for change in schema_changes:
|
|
if not change:
|
|
continue
|
|
active_theory = change[3]
|
|
if not active_theory:
|
|
self.schema.revert(change)
|
|
else:
|
|
self.theories[active_theory].schema.revert(change)
|
|
|
|
def update(self, events):
|
|
"""Apply EVENTS.
|
|
|
|
And return the list of EVENTS that actually
|
|
changed the theory. Each event is the insert or delete of
|
|
a policy statement.
|
|
"""
|
|
changes = []
|
|
self.log(None, "Update %s", utility.iterstr(events))
|
|
try:
|
|
for event in events:
|
|
schema_changes = self.update_rule_schema(
|
|
event.formula, event.insert)
|
|
formula = compile.reorder_for_safety(event.formula)
|
|
if event.insert:
|
|
if self._insert_actual(formula):
|
|
changes.append(event)
|
|
else:
|
|
self.revert_schema(schema_changes)
|
|
else:
|
|
if self._delete_actual(formula):
|
|
changes.append(event)
|
|
else:
|
|
self.revert_schema(schema_changes)
|
|
except Exception:
|
|
LOG.exception("runtime caught an exception")
|
|
raise
|
|
|
|
return changes
|
|
|
|
def update_would_cause_errors(self, events):
|
|
"""Return a list of PolicyException.
|
|
|
|
Return a list of PolicyException if we were
|
|
to apply the insert/deletes of policy statements dictated by
|
|
EVENTS to the current policy.
|
|
"""
|
|
self.log(None, "update_would_cause_errors %s", utility.iterstr(events))
|
|
errors = []
|
|
for event in events:
|
|
if not compile.is_datalog(event.formula):
|
|
errors.append(exception.PolicyException(
|
|
"Non-formula found: {}".format(
|
|
str(event.formula))))
|
|
else:
|
|
if event.formula.is_atom():
|
|
errors.extend(compile.fact_errors(
|
|
event.formula, self.theories, self.name))
|
|
else:
|
|
errors.extend(compile.rule_errors(
|
|
event.formula, self.theories, self.name))
|
|
# Would also check that rules are non-recursive, but that
|
|
# is currently being handled by Runtime. The current implementation
|
|
# disallows recursion in all theories.
|
|
return errors
|
|
|
|
def define(self, rules):
|
|
"""Empties and then inserts RULES."""
|
|
self.empty()
|
|
return self.update([compile.Event(formula=rule, insert=True)
|
|
for rule in rules])
|
|
|
|
def empty(self, tablenames=None, invert=False):
|
|
"""Deletes contents of theory.
|
|
|
|
If provided, TABLENAMES causes only the removal of all rules
|
|
that help define one of the tables in TABLENAMES.
|
|
If INVERT is true, all rules defining anything other than a
|
|
table in TABLENAMES is deleted.
|
|
"""
|
|
if tablenames is None:
|
|
self.rules.clear()
|
|
return
|
|
if invert:
|
|
to_clear = set(self.defined_tablenames()) - set(tablenames)
|
|
else:
|
|
to_clear = tablenames
|
|
for table in to_clear:
|
|
self.rules.clear_table(table)
|
|
|
|
def policy(self):
|
|
# eliminate all rules with empty bodies
|
|
return [p for p in self.content() if len(p.body) > 0]
|
|
|
|
def __contains__(self, formula):
|
|
if compile.is_atom(formula):
|
|
return self.rules.contains(formula.table.table, formula)
|
|
else:
|
|
return self.rules.contains(formula.head.table.table, formula)
|
|
|
|
# Internal Interface
|
|
|
|
def _insert_actual(self, rule):
|
|
"""Insert RULE and return True if there was a change."""
|
|
self.dirty = True
|
|
if compile.is_atom(rule):
|
|
rule = compile.Rule(rule, [], rule.location)
|
|
self.log(rule.head.table.table, "Insert: %s", repr(rule))
|
|
return self.rules.add_rule(rule.head.table.table, rule)
|
|
|
|
def _delete_actual(self, rule):
|
|
"""Delete RULE and return True if there was a change."""
|
|
self.dirty = True
|
|
if compile.is_atom(rule):
|
|
rule = compile.Rule(rule, [], rule.location)
|
|
self.log(rule.head.table.table, "Delete: %s", rule)
|
|
return self.rules.discard_rule(rule.head.table.table, rule)
|
|
|
|
def content(self, tablenames=None):
|
|
if tablenames is None:
|
|
tablenames = self.rules.keys()
|
|
results = []
|
|
for table in tablenames:
|
|
if table in self.rules:
|
|
results.extend(self.rules.get_rules(table))
|
|
return results
|
|
|
|
|
|
class NonrecursiveRuleTheory(RuleHandlingMixin, topdown.TopDownTheory):
|
|
"""A non-recursive collection of Rules."""
|
|
|
|
def __init__(self, name=None, abbr=None,
|
|
schema=None, theories=None, desc=None, owner=None):
|
|
super(NonrecursiveRuleTheory, self).__init__(
|
|
name=name, abbr=abbr, theories=theories, schema=schema,
|
|
desc=desc, owner=owner)
|
|
# dictionary from table name to list of rules with that table in head
|
|
self.rules = ruleset.RuleSet()
|
|
self.kind = base.NONRECURSIVE_POLICY_TYPE
|
|
if schema is None:
|
|
self.schema = compile.Schema()
|
|
# Indicates that a rule was added/removed
|
|
# Used by the compiler to know if a theory should be recompiled.
|
|
self.dirty = False
|
|
|
|
# SELECT implemented by TopDownTheory
|
|
|
|
def head_index(self, table, match_literal=None):
|
|
"""Return head index.
|
|
|
|
This routine must return all the formulas pertinent for
|
|
top-down evaluation when a literal with TABLE is at the top
|
|
of the stack.
|
|
"""
|
|
if table in self.rules:
|
|
return self.rules.get_rules(table, match_literal)
|
|
return []
|
|
|
|
def arity(self, table, modal=None):
|
|
"""Return the number of arguments TABLENAME takes.
|
|
|
|
:param table: can be either a string or a Tablename
|
|
:returns: None if arity is unknown (if it does not occur in
|
|
the head of a rule).
|
|
"""
|
|
if isinstance(table, compile.Tablename):
|
|
service = table.service
|
|
name = table.table
|
|
fullname = table.name()
|
|
else:
|
|
fullname = table
|
|
service, name = compile.Tablename.parse_service_table(table)
|
|
# check if schema knows the answer
|
|
if self.schema:
|
|
if service is None or service == self.name:
|
|
arity = self.schema.arity(name)
|
|
else:
|
|
arity = self.schema.arity(fullname)
|
|
if arity is not None:
|
|
return arity
|
|
# assuming a single arity for all tables
|
|
formulas = self.head_index(fullname) or self.head_index(name)
|
|
try:
|
|
first = next(f for f in formulas
|
|
if f.head.table.matches(service, name, modal))
|
|
except StopIteration:
|
|
return None
|
|
# should probably have an overridable function for computing
|
|
# the arguments of a head. Instead we assume heads have .arguments
|
|
return len(self.head(first).arguments)
|
|
|
|
def defined_tablenames(self):
|
|
"""Returns list of table names defined in/written to this theory."""
|
|
return self.rules.keys()
|
|
|
|
def head(self, formula):
|
|
"""Given the output from head_index(), return the formula head.
|
|
|
|
Given a FORMULA, return the thing to unify against.
|
|
Usually, FORMULA is a compile.Rule, but it could be anything
|
|
returned by HEAD_INDEX.
|
|
"""
|
|
return formula.head
|
|
|
|
def body(self, formula):
|
|
"""Return formula body.
|
|
|
|
Given a FORMULA, return a list of things to push onto the
|
|
top-down eval stack.
|
|
"""
|
|
return formula.body
|
|
|
|
|
|
class ActionTheory(NonrecursiveRuleTheory):
|
|
"""ActionTheory object.
|
|
|
|
Same as NonrecursiveRuleTheory except it has fewer constraints
|
|
on the permitted rules. Still working out the details.
|
|
"""
|
|
def __init__(self, name=None, abbr=None,
|
|
schema=None, theories=None, desc=None, owner=None):
|
|
super(ActionTheory, self).__init__(name=name, abbr=abbr,
|
|
schema=schema, theories=theories,
|
|
desc=desc, owner=owner)
|
|
self.kind = base.ACTION_POLICY_TYPE
|
|
|
|
def update_would_cause_errors(self, events):
|
|
"""Return a list of PolicyException.
|
|
|
|
Return a list of PolicyException if we were
|
|
to apply the events EVENTS to the current policy.
|
|
"""
|
|
self.log(None, "update_would_cause_errors %s", utility.iterstr(events))
|
|
errors = []
|
|
for event in events:
|
|
if not compile.is_datalog(event.formula):
|
|
errors.append(exception.PolicyException(
|
|
"Non-formula found: {}".format(
|
|
str(event.formula))))
|
|
else:
|
|
if event.formula.is_atom():
|
|
errors.extend(compile.fact_errors(
|
|
event.formula, self.theories, self.name))
|
|
else:
|
|
errors.extend(compile.rule_head_has_no_theory(
|
|
event.formula,
|
|
permit_head=lambda lit: lit.is_update()))
|
|
# Should put this back in place, but there are some
|
|
# exceptions that we don't handle right now.
|
|
# Would like to mark some tables as only being defined
|
|
# for certain bound/free arguments and take that into
|
|
# account when doing error checking.
|
|
# errors.extend(compile.rule_negation_safety(event.formula))
|
|
return errors
|
|
|
|
|
|
class MultiModuleNonrecursiveRuleTheory(NonrecursiveRuleTheory):
|
|
"""MultiModuleNonrecursiveRuleTheory object.
|
|
|
|
Same as NonrecursiveRuleTheory, except we allow rules with theories
|
|
in the head. Intended for use with TopDownTheory's INSTANCES method.
|
|
"""
|
|
def _insert_actual(self, rule):
|
|
"""Insert RULE and return True if there was a change."""
|
|
if compile.is_atom(rule):
|
|
rule = compile.Rule(rule, [], rule.location)
|
|
self.log(rule.head.table.table, "Insert: %s", rule)
|
|
return self.rules.add_rule(rule.head.table.table, rule)
|
|
|
|
def _delete_actual(self, rule):
|
|
"""Delete RULE and return True if there was a change."""
|
|
if compile.is_atom(rule):
|
|
rule = compile.Rule(rule, [], rule.location)
|
|
self.log(rule.head.table.table, "Delete: %s", rule)
|
|
return self.rules.discard_rule(rule.head.table.table, rule)
|
|
|
|
# def update_would_cause_errors(self, events):
|
|
# return []
|
|
|
|
|
|
class DatasourcePolicyTheory(NonrecursiveRuleTheory):
|
|
"""DatasourcePolicyTheory
|
|
|
|
DatasourcePolicyTheory is identical to NonrecursiveRuleTheory, except that
|
|
self.kind is base.DATASOURCE_POLICY_TYPE instead of
|
|
base.NONRECURSIVE_POLICY_TYPE. DatasourcePolicyTheory uses a different
|
|
self.kind so that the synchronizer knows not to synchronize policies of
|
|
kind DatasourcePolicyTheory with the database listing of policies.
|
|
"""
|
|
|
|
def __init__(self, name=None, abbr=None,
|
|
schema=None, theories=None, desc=None, owner=None):
|
|
super(DatasourcePolicyTheory, self).__init__(
|
|
name=name, abbr=abbr, theories=theories, schema=schema,
|
|
desc=desc, owner=owner)
|
|
self.kind = base.DATASOURCE_POLICY_TYPE
|