422 lines
17 KiB
Python
422 lines
17 KiB
Python
# Copyright (c) 2013 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
|
|
|
|
import os
|
|
|
|
from oslo_log import log as logging
|
|
|
|
from congress.datalog import compile
|
|
from congress.datalog import unify
|
|
from congress.datalog import utility
|
|
from congress.policy_engines import agnostic
|
|
from congress.tests import base
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
NREC_THEORY = 'non-recursive theory'
|
|
DB_THEORY = 'database'
|
|
MAT_THEORY = 'materialized'
|
|
|
|
|
|
# This file contains tests that are likely broken. But the tests
|
|
# are good ones once we get the underlying data structures fixed.
|
|
# TODO(thinrichs): fix tests so they are working again.
|
|
|
|
class TestRuntime(base.TestCase):
|
|
|
|
def prep_runtime(self, code=None, msg=None, target=None):
|
|
# compile source
|
|
if msg is not None:
|
|
LOG.debug(msg)
|
|
if code is None:
|
|
code = ""
|
|
if target is None:
|
|
target = MAT_THEORY
|
|
run = agnostic.Runtime()
|
|
run.theory[NREC_THEORY] = agnostic.NonrecursiveRuleTheory()
|
|
run.theory[DB_THEORY] = agnostic.Database()
|
|
run.theory[MAT_THEORY] = agnostic.MaterializedViewTheory()
|
|
run.debug_mode()
|
|
run.insert(code, target=target)
|
|
return run
|
|
|
|
def check_class(self, run, correct_database_code, msg=None):
|
|
"""Test MAT_THEORY.
|
|
|
|
Check that runtime RUN's MAT_THEORY theory
|
|
has exactly the same contents as CORRECT_DATABASE_CODE.
|
|
"""
|
|
self.open(msg)
|
|
db_class = run.theory[MAT_THEORY].database
|
|
# self.showdb(run)
|
|
correct = agnostic.string_to_database(correct_database_code)
|
|
self.check_db_diffs(db_class, correct, msg)
|
|
self.close(msg)
|
|
|
|
def check_db(self, run, correct_database_code, msg=None):
|
|
"""Test DB_THEORY.
|
|
|
|
Check that runtime RUN.theory[DB_THEORY] is
|
|
equal to CORRECT_DATABASE_CODE.
|
|
"""
|
|
# extract correct answer from correct_database_code
|
|
self.open(msg)
|
|
correct_database = agnostic.string_to_database(correct_database_code)
|
|
self.check_db_diffs(run.theory[DB_THEORY],
|
|
correct_database, msg)
|
|
self.close(msg)
|
|
|
|
def check_db_diffs(self, actual, correct, msg):
|
|
extra = actual - correct
|
|
missing = correct - actual
|
|
extra = [e for e in extra if not e[0].startswith("___")]
|
|
missing = [m for m in missing if not m[0].startswith("___")]
|
|
self.output_diffs(extra, missing, msg, actual=actual)
|
|
|
|
def output_diffs(self, extra, missing, msg, actual=None):
|
|
if len(extra) > 0:
|
|
LOG.debug("Extra tuples")
|
|
LOG.debug(", ".join([str(x) for x in extra]))
|
|
if len(missing) > 0:
|
|
LOG.debug("Missing tuples")
|
|
LOG.debug(", ".join([str(x) for x in missing]))
|
|
if len(extra) > 0 or len(missing) > 0:
|
|
LOG.debug("Resulting database: %s", actual)
|
|
self.assertTrue(len(extra) == 0 and len(missing) == 0, msg)
|
|
|
|
def check_equal(self, actual_code, correct_code, msg=None, equal=None):
|
|
def minus(iter1, iter2, invert=False):
|
|
extra = []
|
|
for i1 in iter1:
|
|
found = False
|
|
for i2 in iter2:
|
|
# for asymmetric equality checks
|
|
if invert:
|
|
test_result = equal(i2, i1)
|
|
else:
|
|
test_result = equal(i1, i2)
|
|
if test_result:
|
|
found = True
|
|
break
|
|
if not found:
|
|
extra.append(i1)
|
|
return extra
|
|
if equal is None:
|
|
equal = lambda x, y: x == y
|
|
LOG.debug("** Checking equality: %s **", msg)
|
|
actual = compile.parse(actual_code)
|
|
correct = compile.parse(correct_code)
|
|
extra = minus(actual, correct)
|
|
# in case EQUAL is asymmetric, always supply actual as the first arg
|
|
missing = minus(correct, actual, invert=True)
|
|
self.output_diffs(extra, missing, msg)
|
|
LOG.debug("** Finished equality: %s **", msg)
|
|
|
|
def check_same(self, actual_code, correct_code, msg=None):
|
|
"""Checks if ACTUAL_CODE is a variable-renaming of CORRECT_CODE."""
|
|
return self.check_equal(
|
|
actual_code, correct_code, msg=msg,
|
|
equal=lambda x, y: unify.same(x, y) is not None)
|
|
|
|
def check_instance(self, actual_code, correct_code, msg=None):
|
|
"""Checks if ACTUAL_CODE is an instance of CORRECT_CODE."""
|
|
return self.check_equal(
|
|
actual_code, correct_code, msg=msg,
|
|
equal=lambda x, y: unify.instance(x, y) is not None)
|
|
|
|
def check_proofs(self, run, correct, msg=None):
|
|
"""Test proofs.
|
|
|
|
Check that the proofs stored in runtime RUN are exactly
|
|
those in CORRECT.
|
|
"""
|
|
# example
|
|
# check_proofs(run, {'q': {(1,):
|
|
# Database.ProofCollection([{'x': 1, 'y': 2}])}})
|
|
|
|
errs = []
|
|
checked_tables = set()
|
|
for table in run.database.table_names():
|
|
if table in correct:
|
|
checked_tables.add(table)
|
|
for dbtuple in run.database[table]:
|
|
if dbtuple.tuple in correct[table]:
|
|
if dbtuple.proofs != correct[table][dbtuple.tuple]:
|
|
errs.append(
|
|
"For table {} tuple {}\n Computed: {}\n "
|
|
"Correct: {}".format(
|
|
table, str(dbtuple),
|
|
str(dbtuple.proofs),
|
|
str(correct[table][dbtuple.tuple])))
|
|
for table in set(correct.keys()) - checked_tables:
|
|
errs.append("Table {} had a correct answer but did not exist "
|
|
"in the database".format(table))
|
|
if len(errs) > 0:
|
|
# LOG.debug("Check_proof errors:\n%s", "\n".join(errs))
|
|
self.fail("\n".join(errs))
|
|
|
|
def showdb(self, run):
|
|
LOG.debug("Resulting DB: %s",
|
|
run.theory[run.CLASSIFY_THEORY].database |
|
|
run.theory[run.DATABASE] |
|
|
run.theory[run.ENFORCEMENT_THEORY].database)
|
|
|
|
def insert(self, run, alist, target=None):
|
|
if target is None:
|
|
target = MAT_THEORY
|
|
run.insert(tuple(alist))
|
|
|
|
def delete(self, run, alist):
|
|
run.delete(tuple(alist))
|
|
|
|
def test_remediation(self):
|
|
"""Test remediation computation."""
|
|
def check(action_code, classify_code, query, correct, msg):
|
|
run = self.prep_runtime()
|
|
actth = run.ACTION_THEORY
|
|
clsth = run.CLASSIFY_THEORY
|
|
run.insert(action_code, target=actth)
|
|
run.insert(class_code, target=clsth)
|
|
self.showdb(run)
|
|
self.check_equal(run.remediate(query), correct, msg)
|
|
|
|
# simple
|
|
action_code = ('action("a")'
|
|
'p-(x) :- a(x)')
|
|
class_code = ('err(x) :- p(x)'
|
|
'p(1)')
|
|
check(action_code, class_code, 'err(1)', 'p-(1) :- a(1)', 'Monadic')
|
|
|
|
# rules in action theory
|
|
action_code = ('action("a")'
|
|
'p-(x) :- q(x)'
|
|
'q(x) :- a(x)')
|
|
class_code = ('err(x) :- p(x)'
|
|
'p(1)')
|
|
check(action_code, class_code, 'err(1)', 'p-(1) :- a(1)',
|
|
'Monadic, indirect')
|
|
|
|
# multiple conditions in error
|
|
action_code = ('action("a")'
|
|
'action("b")'
|
|
'p-(x) :- a(x)'
|
|
'q-(x) :- b(x)')
|
|
class_code = ('err(x) :- p(x), q(x)'
|
|
'p(1)'
|
|
'q(1)')
|
|
check(action_code, class_code, 'err(1)',
|
|
'p-(1) :- a(1) q-(1) :- b(1)',
|
|
'Monadic, two conditions, two actions')
|
|
|
|
def test_access_control(self):
|
|
"""Test access control: whether a given action is permitted."""
|
|
def create(ac_code, class_code):
|
|
run = self.prep_runtime()
|
|
|
|
acth = run.ACCESSCONTROL_THEORY
|
|
permitted, errors = run.insert(ac_code, target=acth)
|
|
self.assertTrue(permitted,
|
|
"Error in access control policy: {}".format(
|
|
utility.iterstr(errors)))
|
|
|
|
clsth = run.CLASSIFY_THEORY
|
|
permitted, errors = run.insert(class_code, target=clsth)
|
|
self.assertTrue(permitted, "Error in classifier policy: {}".format(
|
|
utility.iterstr(errors)))
|
|
return run
|
|
|
|
def check_true(run, query, support='', msg=None):
|
|
result = run.access_control(query, support)
|
|
self.assertTrue(result,
|
|
"Error in access control test {}".format(msg))
|
|
|
|
def check_false(run, query, support='', msg=None):
|
|
result = run.access_control(query, support)
|
|
self.assertFalse(result,
|
|
"Error in access control test {}".format(msg))
|
|
|
|
# Only checking basic I/O interface for the access_control request.
|
|
# Basic inference algorithms are tested elsewhere.
|
|
|
|
# Simple
|
|
ac_code = ('action(x) :- q(x)')
|
|
classify_code = 'q(2)'
|
|
run = create(ac_code, classify_code)
|
|
check_true(run, "action(2)", msg="Simple true action")
|
|
check_false(run, "action(1)", msg="Simple false action")
|
|
|
|
# Options
|
|
ac_code = ('action(x, y) :- q(x), options:value(y, "name", name), '
|
|
'r(name)')
|
|
classify_code = 'q(2) r("alice")'
|
|
run = create(ac_code, classify_code)
|
|
check_true(run, 'action(2,18)', 'options:value(18, "name", "alice")',
|
|
msg="Single option true")
|
|
check_false(run, 'action(2,18)', 'options:value(18, "name", "bob")',
|
|
msg="Single option false")
|
|
|
|
# Multiple Options
|
|
ac_code = ('action(x, y) :- q(x), options:value(y, "name", name), '
|
|
'r(name), options:value(y, "age", 30)')
|
|
classify_code = 'q(2) r("alice")'
|
|
run = create(ac_code, classify_code)
|
|
check_true(run, 'action(2,18)', 'options:value(18, "name", "alice") '
|
|
'options:value(18, "age", 30)', msg="Multiple option true")
|
|
check_false(run, 'action(2, 18)', 'options:value(18, "name", "bob") '
|
|
'options:value(18, "age", 30)',
|
|
msg="Multiple option false")
|
|
|
|
def test_enforcement(self):
|
|
"""Test enforcement."""
|
|
def prep_runtime(enforce_theory, action_theory, class_theory):
|
|
run = agnostic.Runtime()
|
|
run.insert(enforce_theory, target=run.ENFORCEMENT_THEORY)
|
|
run.insert(action_theory, target=run.ACTION_THEORY)
|
|
run.insert(class_theory, target=run.CLASSIFY_THEORY)
|
|
return run
|
|
enforce = 'act(x) :- p(x)'
|
|
action = 'action("act")'
|
|
run = prep_runtime(enforce, action, "")
|
|
run.insert('p(1)')
|
|
self.check_equal(run.logger.content(), 'act(1)', 'Insert')
|
|
run.logger.empty()
|
|
run.insert('p(1)')
|
|
self.check_equal(run.logger.content(), '', 'Insert again')
|
|
run.insert('p(2)')
|
|
self.check_equal(run.logger.content(), 'act(2)', 'Insert different')
|
|
run.logger.empty()
|
|
run.delete('p(2)')
|
|
self.check_equal(run.logger.content(), '', 'Delete')
|
|
|
|
def test_neutron_actions(self):
|
|
"""Test our encoding of the Neutron actions basics by simulation."""
|
|
def check(query, action_sequence, correct, msg):
|
|
actual = run.simulate(query, action_sequence)
|
|
LOG.debug("Simulate results: %s", actual)
|
|
self.check_instance(actual, correct, msg)
|
|
|
|
full_path = os.path.realpath(__file__)
|
|
path = os.path.dirname(full_path)
|
|
neutron_path = path + "/../../../examples/neutron.action"
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
# load_file does not exist any longer.
|
|
permitted, errs = run.load_file(neutron_path, target=run.ACTION_THEORY)
|
|
if not permitted:
|
|
self.assertTrue(permitted, "Error in Neutron file: {}".format(
|
|
"\n".join([str(x) for x in errs])))
|
|
return
|
|
|
|
# Ports
|
|
query = 'neutron:port(x1, x2, x3, x4, x5, x6, x7, x8, x9)'
|
|
acts = 'neutron:create_port("net1", 17), sys:user("tim") :- true'
|
|
correct = ('neutron:port(id, "net1", name, mac, "null",'
|
|
'"null", z, w, "tim")')
|
|
check(query, acts, correct, 'Simple port creation')
|
|
|
|
query = 'neutron:port(x1, x2, x3, x4, x5, x6, x7, x8, x9)'
|
|
# result(uuid): simulation-specific table that holds the results
|
|
# of the last action invocation
|
|
acts = ('neutron:create_port("net1", 17), sys:user("tim") :- true '
|
|
'neutron:update_port(uuid, 18), sys:user("tim"), '
|
|
' options:value(18, "name", "tims port") :- result(uuid) ')
|
|
correct = ('neutron:port(id, "net1", "tims port", mac, "null",'
|
|
'"null", z, w, "tim")')
|
|
check(query, acts, correct, 'Port create, update')
|
|
|
|
query = 'neutron:port(x1, x2, x3, x4, x5, x6, x7, x8, x9)'
|
|
# result(uuid): simulation-specific table that holds the results
|
|
# of the last action invocation
|
|
acts = ('neutron:create_port("net1", 17), sys:user("tim") :- true '
|
|
'neutron:update_port(uuid, 18), sys:user("tim"), '
|
|
' options:value(18, "name", "tims port") :- result(uuid) '
|
|
'neutron:delete_port(uuid), sys:user("tim")'
|
|
' :- result(uuid) ')
|
|
correct = ''
|
|
check(query, acts, correct, 'Port create, update, delete')
|
|
|
|
# Networks
|
|
query = ('neutron:network(id, name, status, admin_state, shared,'
|
|
'tenenant_id)')
|
|
acts = 'neutron:create_network(17), sys:user("tim") :- true'
|
|
correct = 'neutron:network(id, "", status, "true", "true", "tim")'
|
|
check(query, acts, correct, 'Simple network creation')
|
|
|
|
query = ('neutron:network(id, name, status, admin_state, '
|
|
'shared, tenenant_id)')
|
|
acts = ('neutron:create_network(17), sys:user("tim") :- true '
|
|
'neutron:update_network(uuid, 18), sys:user("tim"), '
|
|
' options:value(18, "admin_state", "false") :- result(uuid)')
|
|
correct = 'neutron:network(id, "", status, "false", "true", "tim")'
|
|
check(query, acts, correct, 'Network creation, update')
|
|
|
|
query = ('neutron:network(id, name, status, admin_state, shared, '
|
|
'tenenant_id)')
|
|
acts = ('neutron:create_network(17), sys:user("tim") :- true '
|
|
'neutron:update_network(uuid, 18), sys:user("tim"), '
|
|
' options:value(18, "admin_state", "false") :- result(uuid)'
|
|
'neutron:delete_network(uuid) :- result(uuid)')
|
|
correct = ''
|
|
check(query, acts, correct, 'Network creation, update')
|
|
|
|
# Subnets
|
|
query = ('neutron:subnet(id, name, network_id, '
|
|
'gateway_ip, ip_version, cidr, enable_dhcp, tenant_id)')
|
|
acts = ('neutron:create_subnet("net1", "10.0.0.1/24", 17), '
|
|
'sys:user("tim") :- true')
|
|
correct = ('neutron:subnet(id, "", "net1", gateway_ip, 4, '
|
|
'"10.0.0.1/24", "true", "tim")')
|
|
check(query, acts, correct, 'Simple subnet creation')
|
|
|
|
query = ('neutron:subnet(id, name, network_id, '
|
|
'gateway_ip, ip_version, cidr, enable_dhcp, tenant_id)')
|
|
acts = ('neutron:create_subnet("net1", "10.0.0.1/24", 17), '
|
|
'sys:user("tim") :- true '
|
|
'neutron:update_subnet(uuid, 17), sys:user("tim"), '
|
|
' options:value(17, "enable_dhcp", "false") :- result(uuid)')
|
|
correct = ('neutron:subnet(id, "", "net1", gateway_ip, 4, '
|
|
'"10.0.0.1/24", "false", "tim")')
|
|
check(query, acts, correct, 'Subnet creation, update')
|
|
|
|
query = ('neutron:subnet(id, name, network_id, '
|
|
'gateway_ip, ip_version, cidr, enable_dhcp, tenant_id)')
|
|
acts = ('neutron:create_subnet("net1", "10.0.0.1/24", 17), '
|
|
'sys:user("tim") :- true '
|
|
'neutron:update_subnet(uuid, 17), sys:user("tim"), '
|
|
' options:value(17, "enable_dhcp", "false") :- result(uuid)'
|
|
'neutron:delete_subnet(uuid) :- result(uuid)')
|
|
correct = ''
|
|
check(query, acts, correct, 'Subnet creation, update, delete')
|
|
|
|
|
|
def str2form(formula_string):
|
|
return compile.parse1(formula_string)
|
|
|
|
|
|
def str2pol(policy_string):
|
|
return compile.parse(policy_string)
|
|
|
|
|
|
def pol2str(policy):
|
|
return " ".join(str(x) for x in policy)
|
|
|
|
|
|
def form2str(formula):
|
|
return str(formula)
|