Added command line demo

Also reorganized a bit.  Now we can interact with the runtime using just strings.
Useful if (i) accepting updates over the API wire and (ii) policy statements
will most naturally be sent via strings.

See examples/private_public_network.script for the demo

Issue: #
Change-Id: I5804cbe43bbf3eba8936c9d7f65405417c6203e0
This commit is contained in:
Tim Hinrichs 2013-08-22 15:03:04 -07:00
parent 45fd895926
commit 9ae911e646
6 changed files with 227 additions and 73 deletions

View File

@ -1,12 +1,14 @@
error :- nova:virtual_machine(vm), nova:network(vm, network),
error(vm) :- nova:virtual_machine(vm), nova:network(vm, network),
not neutron:public_network(network),
neutron:owner(network, netowner), nova:owner(vm, vmowner), not same_group(netowner, vmowner)
same_group(user1, user2) :- cms:group(user1, group), cms:group(user2, group)
nova:virtual_machine("vm1")
nova:virtual_machine("vm2")
nova:virtual_machine("vm3")
nova:network("vm1", "net_private")
nova:network("vm2", "net_public")
@ -14,6 +16,7 @@ neutron:public_network("net_public")
nova:owner("vm1", "tim")
nova:owner("vm2", "pete")
nova:owner("vm3", "pierre")
neutron:owner("net_private", "martin")
cms:group("pete", "congress")

View File

@ -0,0 +1,99 @@
A script for a demo.
0) Example policy
"all vms must be attached to public networks or to private networks owned by someone in the same group as the vm owner"
1) Draws on disparate data sources
Schema:
nova:virtual_machine(vm)
nova:network(vm, network)
nova:owner(vm, owner)
neutron:public_network(network)
neutron:owner(network, owner)
cms:group(user, group)
2) Policy
error(vm) :- nova:virtual_machine(vm), nova:network(vm, network),
not neutron:public_network(network),
neutron:owner(network, netowner), nova:owner(vm, vmowner), not same_group(netowner, vmowner)
same-group(user1, user2) :- cms:group(user1, group), cms:group(user2, group)
--- Commands ------------------------------------
cd congress/src/policy
python
>>> import runtime
>>> r = runtime.Runtime()
>>> r.load_file("../../examples/private_public_network")
-------------------------------------------------
3) Are there any violations? Not yet.
--- Commands ------------------------------------
>>> print r.select("error(x)")
-------------------------------------------------
4) Change some data to create an error: remove "tim" from group "congress"
--- Commands ------------------------------------
>>> r.delete('cms:group("tim", "congress")')
-------------------------------------------------
5) Check for violations
--- Commands ------------------------------------
>>> print r.select("error(x)")
error(vm1)
-------------------------------------------------
6) Explain the violation:
--- Commands ------------------------------------
>>> print r.explain('error("vm1")')
error(vm1)
nova:virtual_machine(vm1)
nova:network(vm1, net_private)
not neutron:public_network(net_private)
neutron:owner(net_private, martin)
nova:owner(vm1, tim)
not same_group(martin, tim)
-------------------------------------------------
7) Insert new rules: "Error if vm without a network"
--- Commands ------------------------------------
>>> r.insert('error(vm) :- nova:virtual_machine(vm), not is_some_network(vm)'
'is_some_network(vm) :- nova:network(vm, x)')
-------------------------------------------------
8) Check for violations
--- Commands ------------------------------------
>>> print r.select("error(x)")
error(vm1) error(vm3)
-------------------------------------------------
9) Explain the new violation
--- Commands ------------------------------------
>>> print r.explain('error("vm3")')
error(vm3)
nova:virtual_machine(vm3)
not is_some_network(vm3)
-------------------------------------------------

View File

@ -46,7 +46,7 @@ formula_terminator
bare_formula
: rule
| formula
| atom
;
rule
@ -100,10 +100,6 @@ propositional_constant
: ID
;
SYMBOL
: ':' ID
;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
;

View File

@ -247,9 +247,9 @@ class Compiler (object):
# parse input file and convert to internal representation
self.raw_syntax_tree = CongressSyntax.parse_file(input,
input_string=input_string)
#self.print_parse_result()
# self.print_parse_result()
self.theory = CongressSyntax.create(self.raw_syntax_tree)
#print str(self)
# print str(self)
def print_parse_result(self):
print_tree(
@ -495,17 +495,17 @@ def print_tree(tree, text, kids, ind=0):
def get_compiled(args):
""" Run compiler as per ARGS and return the resulting Compiler instance. """
# assumes script name is not passed
parser = optparse.OptionParser()
parser.add_option("--input_string", dest="input_string", default=False,
action="store_true",
help="Indicates that inputs should be treated not as file names but "
"as the contents to compile")
(options, inputs) = parser.parse_args(args)
if len(inputs) != 1:
parser.error("Usage: %prog [options] policy-file")
compiler = Compiler()
for i in inputs:
compiler.read_source(i, input_string=options.input_string)
logging.debug(str(compiler.theory))
compiler.compute_delta_rules()
return compiler
@ -520,7 +520,12 @@ def get_runtime(args):
run.database.tracer = tracer
return run
# if __name__ == '__main__':
# main()
def main(args):
c = get_compiled(args)
for formula in c.theory:
print str(c)
if __name__ == '__main__':
main(sys.argv[1:])

View File

@ -44,7 +44,10 @@ class DeltaRule(object):
class DeltaRuleTheory (object):
""" A collection of DeltaRules. """
def __init__(self, rules=None):
# dictionary from table name to list of rules with that table as trigger
self.contents = {}
# dictionary from table name to number of rules with that table in head
self.views = {}
if rules is not None:
for rule in rules:
self.insert(rule)
@ -56,22 +59,27 @@ class DeltaRuleTheory (object):
return self.delete(delta)
def insert(self, delta):
if delta.head.table in self.views:
self.views[delta.head.table] += 1
else:
self.views[delta.head.table] = 1
if delta.trigger.table not in self.contents:
self.contents[delta.trigger.table] = [delta]
else:
self.contents[delta.trigger.table].append(delta)
def delete(self, delta):
if delta.head.table in self.views:
self.views[delta.head.table] -= 1
if self.views[delta.head.table] == 0:
del self.views[delta.head.table]
if delta.trigger.table not in self.contents:
return
self.contents[delta.trigger.table].remove(delta)
def __str__(self):
return str(self.contents)
# for table in self.contents:
# print "{}:".format(table)
# for rule in self.delta_rules[table]:
# print " {}".format(rule)
def rules_with_trigger(self, table):
if table not in self.contents:
@ -79,6 +87,9 @@ class DeltaRuleTheory (object):
else:
return self.contents[table]
def is_view(self, x):
return x in self.views
##############################################################################
## Events
##############################################################################
@ -420,7 +431,7 @@ class Runtime (object):
return s
def __init__(self, rules):
def __init__(self, rules=None):
# rules dictating how an insert/delete to one table
# affects other tables
self.delta_rules = DeltaRuleTheory(rules)
@ -435,35 +446,110 @@ class Runtime (object):
self.tracer.log(table, "RT: " + msg, depth)
############### External interface ###############
def load_file(self, filename):
""" Compile the given FILENAME and insert each of the statements
into the runtime. """
compiler = compile.get_compiled([filename])
for formula in compiler.theory:
self.insert_obj(formula)
def select(self, query):
""" Event handler for arbitrary queries. Returns the set of
all instantiated QUERY that are true. """
# should generalize to at least a (conjunction of atoms)
# Need to change compiler a bit, but runtime should be fine.
assert isinstance(query, compile.Atom), "Only have support for atomic queries"
return self.database.select(query)
if isinstance(query, basestring):
return self.select_string(query)
else:
return self.select_obj(query)
def select_if(self, query, temporary_data):
""" Event handler for hypothetical queries. Returns the set of
all instantiated QUERYs that would be true IF
TEMPORARY_DATA were true. """
assert False, "Not yet implemented"
if isinstance(query, basestring):
return self.select_if_string(query, temporary_data)
else:
return self.select_if_obj(query, temporary_data)
def explain(self, query):
""" Event handler for explanations. Given a ground query, return
a single proof that it belongs in the database. """
assert isinstance(query, compile.Atom), "Only have support for literals"
return self.explain_aux(query, 0)
if isinstance(query, basestring):
return self.explain_string(query)
else:
return self.explain_obj(query)
def insert(self, formula):
""" Event handler for arbitrary insertion (rules and facts). """
return self.modify(formula, is_insert=True)
if isinstance(formula, basestring):
return self.insert_string(formula)
else:
return self.insert_obj(formula)
def delete(self, formula):
""" Event handler for arbitrary deletion (rules and facts). """
if isinstance(formula, basestring):
return self.delete_string(formula)
else:
return self.delete_obj(formula)
############### External typed interface ###############
def select_obj(self, query):
# should generalize to at least a (conjunction of atoms)
# Need to change compiler a bit, but runtime should be fine.
assert isinstance(query, compile.Atom), "Only have support for atomic queries"
return self.database.select(query)
def select_string(self, policy_string):
def str_tuple_atom (atom):
s = atom[0]
s += '('
s += ', '.join([str(x) for x in atom[1:]])
s += ')'
return s
c = compile.get_compiled(['--input_string', policy_string])
assert len(c.theory) == 1, "Queries can have only 1 statement: {}".format(
[str(x) for x in c.theory])
assert c.theory[0].is_atom(), "Queries must be atomic"
results = self.select_obj(c.theory[0])
return " ".join([str_tuple_atom(x) for x in results])
def select_if_obj(self, query, temporary_data):
assert False, "Not yet implemented"
def select_if_string(self, query_string, temporary_data):
assert False, "Not yet implemented"
def explain_obj(self, query):
assert isinstance(query, compile.Atom), "Only have support for literals"
return self.explain_aux(query, 0)
def explain_string(self, query_string):
c = compile.get_compiled([query_string, '--input_string'])
assert len(c.theory) == 1, "Queries can have only 1 statement"
assert c.theory[0].is_atom(), "Queries must be atomic"
results = self.explain_obj(c.theory[0])
return str(results)
def insert_obj(self, formula):
return self.modify(formula, is_insert=True)
def insert_string(self, policy_string):
c = compile.get_compiled([policy_string, '--input_string'])
for formula in c.theory:
logging.debug("Parsed {}".format(str(formula)))
self.insert_obj(formula)
def delete_obj(self, formula):
return self.modify(formula, is_insert=False)
def delete_string(self, policy_string):
c = compile.get_compiled([policy_string, '--input_string'])
for formula in c.theory:
self.delete_obj(formula)
############### Interface implementation ###############
def explain_aux(self, query, depth):
self.log(query.table, "Explaining {}".format(str(query)), depth)
if query.is_negated():
@ -485,12 +571,17 @@ class Runtime (object):
def modify(self, formula, is_insert=True):
""" Event handler for arbitrary insertion/deletion (rules and facts). """
if formula.is_atom():
args = tuple([arg.name for arg in formula.arguments])
self.modify_tuple(formula.table, args, is_insert=is_insert)
if self.delta_rules.is_view(formula.table):
return self.view_update_options(formula)
else:
args = tuple([arg.name for arg in formula.arguments])
self.modify_tuple(formula.table, args, is_insert=is_insert)
return None
else:
self.modify_rule(formula, is_insert=is_insert)
for delta_rule in compile.compute_delta_rules([formula]):
self.delta_rules.modify(delta_rule, is_insert=is_insert)
return None
def modify_rule(self, rule, is_insert):
""" Add rule (not a DeltaRule) to collection and update
@ -593,50 +684,9 @@ class Runtime (object):
proofs=new_tuples[new_tuple],
insert=insert))
class StringRuntime(Runtime):
""" Version of Runtime that communicates via strings. """
def select(self, policy_string):
""" Event handler for arbitrary queries. Returns the set of
all instantiated POLICY_STRING that are true. """
def str_tuple_atom (atom):
s = atom[0]
s += '('
s += ', '.join([str(x) for x in atom[1:]])
s += ')'
return s
c = compile.get_compiled([policy_string, '--input_string'])
assert len(c.theory) == 1, "Queries can have only 1 statement"
assert c.theory[0].is_atom(), "Queries must be atomic"
results = super(StringRuntime, self).select(c.theory[0])
return " ".join([str_tuple_atom(x) for x in results])
def select_if(self, query_string, temporary_data):
""" Event handler for hypothetical queries. Returns the set of
all instantiated QUERYs that would be true IF
TEMPORARY_DATA were true. """
assert False, "Not yet implemented"
def explain(self, query_string):
""" Event handler for explanations. Given a ground query, return
all explanations for it. """
c = compile.get_compiled([query_string, '--input_string'])
assert len(c.theory) == 1, "Queries can have only 1 statement"
assert c.theory[0].is_atom(), "Queries must be atomic"
results = super(StringRuntime, self).explain(c.theory[0])
return str(results)
def insert(self, policy_string):
""" Event handler for arbitrary insertion (rules and/or facts). """
c = compile.get_compiled([policy_string, '--input_string'])
for formula in c.theory:
logging.debug("Parsed {}".format(str(formula)))
super(StringRuntime, self).insert(formula)
def delete(self, policy_string):
""" Event handler for arbitrary deletion (rules and/or facts). """
c = compile.get_compiled([policy_string, '--input_string'])
for formula in c.theory:
super(StringRuntime, self).delete(formula)
############### View updates ###############
def view_update_options(self):
return [None]
def plug(atom, binding, withtable=False):

View File

@ -17,7 +17,7 @@ class TestRuntime(unittest.TestCase):
if msg is not None:
logging.debug(msg)
c = compile.get_compiled([code, '--input_string'])
run = runtime.StringRuntime(c.delta_rules)
run = runtime.Runtime(c.delta_rules)
tracer = runtime.Tracer()
tracer.trace('*')
run.tracer = tracer
@ -424,6 +424,7 @@ class TestRuntime(unittest.TestCase):
run.delete("p(x) :- q(x), r(x)")
self.check(run, 'q(1) r(1) q(2) r(2)', "Delete rule")
# TODO(tim): add tests for explanations
def test_explanations(self):
""" Test the explanation event handler. """
run = self.prep_runtime("p(x) :- q(x), r(x)", "Explanations")
@ -435,7 +436,7 @@ class TestRuntime(unittest.TestCase):
run.insert("s(1) r(1) t(1)")
self.showdb(run)
logging.debug(run.explain("p(1)"))
self.fail()
# self.fail()
if __name__ == '__main__':
unittest.main()