cd9aa33451
harness.py loads library policy from disk files to DB uniqueness constraint added on library policy name. devstack plugin updated to install library policy files to default location updated congress stand-alone install instruction Partially implements: blueprint policy-library Closes-Bug: 1693619 Closes-Bug: 1693672 Change-Id: I51097081f6576755751231feb5ed2b0be642d91e
487 lines
19 KiB
Python
487 lines
19 KiB
Python
#
|
|
# Copyright (c) 2014 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.
|
|
|
|
"""
|
|
test_congress
|
|
----------------------------------
|
|
|
|
Tests for `congress` module.
|
|
"""
|
|
from __future__ import print_function
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
|
|
import mock
|
|
import neutronclient.v2_0
|
|
from oslo_log import log as logging
|
|
|
|
from congress.api import base as api_base
|
|
from congress.common import config
|
|
from congress.datasources import neutronv2_driver
|
|
from congress.datasources import nova_driver
|
|
from congress.db import db_library_policies
|
|
from congress.tests.api import base as tests_api_base
|
|
from congress.tests import base
|
|
from congress.tests.datasources import test_neutron_driver as test_neutron
|
|
from congress.tests import helper
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class BaseTestPolicyCongress(base.SqlTestCase):
|
|
|
|
def setUp(self):
|
|
super(BaseTestPolicyCongress, self).setUp()
|
|
self.services = tests_api_base.setup_config(with_fake_datasource=False)
|
|
self.api = self.services['api']
|
|
self.node = self.services['node']
|
|
self.engine = self.services['engine']
|
|
self.library = self.services['library']
|
|
|
|
self.neutronv2 = self._create_neutron_mock('neutron')
|
|
|
|
def tearDown(self):
|
|
self.node.stop()
|
|
super(BaseTestPolicyCongress, self).tearDown()
|
|
|
|
def _create_neutron_mock(self, name):
|
|
# Register Neutron service
|
|
args = helper.datasource_openstack_args()
|
|
neutronv2 = neutronv2_driver.NeutronV2Driver(name, args=args)
|
|
# FIXME(ekcs): this is a hack to prevent the synchronizer from
|
|
# attempting to delete this DSD because it's not in DB
|
|
neutronv2.type = 'no_sync_datasource_driver'
|
|
neutron_mock = mock.MagicMock(spec=neutronclient.v2_0.client.Client)
|
|
neutronv2.neutron = neutron_mock
|
|
|
|
# initialize neutron_mocks
|
|
network1 = test_neutron.network_response
|
|
port_response = test_neutron.port_response
|
|
router_response = test_neutron.router_response
|
|
sg_group_response = test_neutron.security_group_response
|
|
neutron_mock.list_networks.return_value = network1
|
|
neutron_mock.list_ports.return_value = port_response
|
|
neutron_mock.list_routers.return_value = router_response
|
|
neutron_mock.list_security_groups.return_value = sg_group_response
|
|
self.node.register_service(neutronv2)
|
|
return neutronv2
|
|
|
|
|
|
class TestCongress(BaseTestPolicyCongress):
|
|
|
|
def setUp(self):
|
|
"""Setup tests that use multiple mock neutron instances."""
|
|
super(TestCongress, self).setUp()
|
|
|
|
# clear the library policies loaded on startup
|
|
db_library_policies.delete_policies()
|
|
|
|
def tearDown(self):
|
|
super(TestCongress, self).tearDown()
|
|
|
|
def setup_config(self):
|
|
args = ['--config-file', helper.etcdir('congress.conf.test')]
|
|
config.init(args)
|
|
|
|
def test_startup(self):
|
|
self.assertIsNotNone(self.services['api'])
|
|
self.assertIsNotNone(self.services['engine'])
|
|
self.assertIsNotNone(self.services['engine'].node)
|
|
|
|
def test_policy(self):
|
|
self.create_policy('alpha')
|
|
self.insert_rule('q(1, 2) :- true', 'alpha')
|
|
self.insert_rule('q(2, 3) :- true', 'alpha')
|
|
helper.retry_check_function_return_value(
|
|
lambda: sorted(self.query('q', 'alpha')['results'],
|
|
key=lambda x: x['data']),
|
|
sorted([{'data': (1, 2)}, {'data': (2, 3)}],
|
|
key=lambda x: x['data']))
|
|
helper.retry_check_function_return_value(
|
|
lambda: list(self.query('q', 'alpha').keys()),
|
|
['results'])
|
|
|
|
def test_policy_datasource(self):
|
|
self.create_policy('alpha')
|
|
self.create_fake_datasource('fake')
|
|
data = self.node.service_object('fake')
|
|
data.state = {'fake_table': set([(1, 2)])}
|
|
|
|
data.poll()
|
|
self.insert_rule('q(x) :- fake:fake_table(x,y)', 'alpha')
|
|
helper.retry_check_function_return_value(
|
|
lambda: self.query('q', 'alpha'), {'results': [{'data': (1,)}]})
|
|
|
|
# TODO(dse2): enable rules to be inserted before data created.
|
|
# Maybe just have subscription handle errors gracefull when
|
|
# asking for a snapshot and return [].
|
|
# self.insert_rule('p(x) :- fake:fake_table(x)', 'alpha')
|
|
|
|
def create_policy(self, name):
|
|
self.api['api-policy'].add_item({'name': name}, {})
|
|
|
|
def insert_rule(self, rule, policy):
|
|
context = {'policy_id': policy}
|
|
return self.api['api-rule'].add_item(
|
|
{'rule': rule}, {}, context=context)
|
|
|
|
def create_fake_datasource(self, name):
|
|
item = {'name': name,
|
|
'driver': 'fake_datasource',
|
|
'description': 'hello world!',
|
|
'enabled': True,
|
|
'type': None,
|
|
'config': {'auth_url': 'foo',
|
|
'username': 'armax',
|
|
'password': '<hidden>',
|
|
'tenant_name': 'armax'}}
|
|
|
|
return self.api['api-datasource'].add_item(item, params={})
|
|
|
|
def query(self, tablename, policyname):
|
|
context = {'policy_id': policyname,
|
|
'table_id': tablename}
|
|
return self.api['api-row'].get_items({}, context)
|
|
|
|
def test_rule_insert_delete(self):
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
context = {'policy_id': 'alice'}
|
|
(id1, _) = self.api['api-rule'].add_item(
|
|
{'rule': 'p(x) :- plus(y, 1, x), q(y)'}, {}, context=context)
|
|
ds = self.api['api-rule'].get_items({}, context)['results']
|
|
self.assertEqual(len(ds), 1)
|
|
self.api['api-rule'].delete_item(id1, {}, context)
|
|
ds = self.engine.policy_object('alice').content()
|
|
self.assertEqual(len(ds), 0)
|
|
|
|
def test_datasource_request_refresh(self):
|
|
# neutron polls automatically here, which is why register_service
|
|
# starts its service.
|
|
neutron = self.neutronv2
|
|
neutron.stop()
|
|
|
|
self.assertEqual(neutron.refresh_request_queue.qsize(), 0)
|
|
neutron.request_refresh()
|
|
self.assertEqual(neutron.refresh_request_queue.qsize(), 1)
|
|
neutron.start()
|
|
|
|
neutron.request_refresh()
|
|
f = lambda: neutron.refresh_request_queue.qsize()
|
|
helper.retry_check_function_return_value(f, 0)
|
|
|
|
def test_datasource_poll(self):
|
|
neutron = self.neutronv2
|
|
neutron.stop()
|
|
neutron._translate_ports({'ports': []})
|
|
self.assertEqual(len(neutron.state['ports']), 0)
|
|
neutron.start()
|
|
f = lambda: len(neutron.state['ports'])
|
|
helper.retry_check_function_return_value_not_eq(f, 0)
|
|
|
|
def test_library_service(self):
|
|
# NOTE(ekcs): only the most basic test right now, more detailed testing
|
|
# done in test_library_service.py
|
|
res = self.library.get_policies()
|
|
self.assertEqual(res, [])
|
|
|
|
|
|
class APILocalRouting(BaseTestPolicyCongress):
|
|
|
|
def setUp(self):
|
|
super(APILocalRouting, self).setUp()
|
|
|
|
# set up second API+PE node
|
|
self.services = tests_api_base.setup_config(
|
|
with_fake_datasource=False, node_id='testnode2',
|
|
same_partition_as_node=self.node)
|
|
self.api2 = self.services['api']
|
|
self.node2 = self.services['node']
|
|
self.engine2 = self.services['engine']
|
|
self.data = self.services['data']
|
|
|
|
# add different data to two PE instances
|
|
# going directly to agnostic not via API to make sure faulty API
|
|
# routing (subject of the test) would not affect test accuracy
|
|
self.engine.create_policy('policy')
|
|
self.engine2.create_policy('policy')
|
|
self.engine.insert('p(1) :- NOT q()', 'policy')
|
|
# self.engine1.insert('p(1)', 'policy')
|
|
self.engine2.insert('p(2) :- NOT q()', 'policy')
|
|
self.engine2.insert('p(3) :- NOT q()', 'policy')
|
|
|
|
def test_intranode_pe_routing(self):
|
|
for i in range(0, 5): # run multiple times (non-determinism)
|
|
result = self.api['api-row'].get_items(
|
|
{}, {'policy_id': 'policy', 'table_id': 'p'})
|
|
self.assertEqual(len(result['results']), 1)
|
|
result = self.api2['api-row'].get_items(
|
|
{}, {'policy_id': 'policy', 'table_id': 'p'})
|
|
self.assertEqual(len(result['results']), 2)
|
|
|
|
def test_non_PE_service_reachable(self):
|
|
# intranode
|
|
result = self.api['api-row'].get_items(
|
|
{}, {'ds_id': 'neutron', 'table_id': 'ports'})
|
|
self.assertEqual(len(result['results']), 1)
|
|
|
|
# internode
|
|
result = self.api2['api-row'].get_items(
|
|
{}, {'ds_id': 'neutron', 'table_id': 'ports'})
|
|
self.assertEqual(len(result['results']), 1)
|
|
|
|
def test_internode_pe_routing(self):
|
|
'''test reach internode PE when intranode PE not available'''
|
|
self.node.unregister_service(api_base.ENGINE_SERVICE_ID)
|
|
result = self.api['api-row'].get_items(
|
|
{}, {'policy_id': 'policy', 'table_id': 'p'})
|
|
self.assertEqual(len(result['results']), 2)
|
|
result = self.api2['api-row'].get_items(
|
|
{}, {'policy_id': 'policy', 'table_id': 'p'})
|
|
self.assertEqual(len(result['results']), 2)
|
|
|
|
|
|
class TestPolicyExecute(BaseTestPolicyCongress):
|
|
|
|
def setUp(self):
|
|
super(TestPolicyExecute, self).setUp()
|
|
self.nova = self._register_test_datasource('nova')
|
|
|
|
def _register_test_datasource(self, name):
|
|
args = helper.datasource_openstack_args()
|
|
if name == 'nova':
|
|
ds = nova_driver.NovaDriver('nova', args=args)
|
|
if name == 'neutron':
|
|
ds = neutronv2_driver.NeutronV2Driver('neutron', args=args)
|
|
ds.update_from_datasource = mock.MagicMock()
|
|
return ds
|
|
|
|
def test_policy_execute(self):
|
|
class NovaClient(object):
|
|
def __init__(self, testkey):
|
|
self.testkey = testkey
|
|
|
|
def disconnectNetwork(self, arg1):
|
|
LOG.info("disconnectNetwork called on %s", arg1)
|
|
self.testkey = "arg1=%s" % arg1
|
|
|
|
nova_client = NovaClient("testing")
|
|
nova = self.nova
|
|
nova.nova_client = nova_client
|
|
self.node.register_service(nova)
|
|
|
|
# insert rule and data
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
(id1, _) = self.api['api-rule'].add_item(
|
|
{'rule': 'execute[nova:disconnectNetwork(x)] :- q(x)'}, {},
|
|
context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 0)
|
|
(id2, _) = self.api['api-rule'].add_item(
|
|
{'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
ans = "arg1=1"
|
|
f = lambda: nova.nova_client.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
# insert more data
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'q(2)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
ans = "arg1=2"
|
|
f = lambda: nova.nova_client.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
# insert irrelevant data
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'r(3)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
|
|
# delete relevant data
|
|
self.api['api-rule'].delete_item(
|
|
id2, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
|
|
# delete policy rule
|
|
self.api['api-rule'].delete_item(
|
|
id1, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
|
|
def test_policy_execute_data_first(self):
|
|
class NovaClient(object):
|
|
def __init__(self, testkey):
|
|
self.testkey = testkey
|
|
|
|
def disconnectNetwork(self, arg1):
|
|
LOG.info("disconnectNetwork called on %s", arg1)
|
|
self.testkey = "arg1=%s" % arg1
|
|
|
|
nova_client = NovaClient(None)
|
|
nova = self.nova
|
|
nova.nova_client = nova_client
|
|
self.node.register_service(nova)
|
|
|
|
# insert rule and data
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 0)
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'execute[nova:disconnectNetwork(x)] :- q(x)'}, {},
|
|
context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
ans = "arg1=1"
|
|
f = lambda: nova.nova_client.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
def test_policy_execute_dotted(self):
|
|
class NovaClient(object):
|
|
def __init__(self, testkey):
|
|
self.testkey = testkey
|
|
self.servers = ServersClass()
|
|
|
|
class ServersClass(object):
|
|
def __init__(self):
|
|
self.ServerManager = ServerManagerClass()
|
|
|
|
class ServerManagerClass(object):
|
|
def __init__(self):
|
|
self.testkey = None
|
|
|
|
def pause(self, id_):
|
|
self.testkey = "arg1=%s" % id_
|
|
|
|
nova_client = NovaClient(None)
|
|
nova = self.nova
|
|
nova.nova_client = nova_client
|
|
self.node.register_service(nova)
|
|
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'execute[nova:servers.ServerManager.pause(x)] :- q(x)'},
|
|
{}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 0)
|
|
self.api['api-rule'].add_item(
|
|
{'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
ans = "arg1=1"
|
|
f = lambda: nova.nova_client.servers.ServerManager.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
def test_policy_execute_no_args(self):
|
|
class NovaClient(object):
|
|
def __init__(self, testkey):
|
|
self.testkey = testkey
|
|
|
|
def disconnectNetwork(self):
|
|
LOG.info("disconnectNetwork called")
|
|
self.testkey = "noargs"
|
|
|
|
nova_client = NovaClient(None)
|
|
nova = self.nova
|
|
nova.nova_client = nova_client
|
|
self.node.register_service(nova)
|
|
|
|
# Note: this probably isn't the behavior we really want.
|
|
# But at least we have a test documenting that behavior.
|
|
|
|
# insert rule and data
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
(id1, rule1) = self.api['api-rule'].add_item(
|
|
{'rule': 'execute[nova:disconnectNetwork()] :- q(x)'}, {},
|
|
context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 0)
|
|
(id2, rule2) = self.api['api-rule'].add_item(
|
|
{'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
ans = "noargs"
|
|
f = lambda: nova.nova_client.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
# insert more data (which DOES NOT cause an execution)
|
|
(id3, rule3) = self.api['api-rule'].add_item(
|
|
{'rule': 'q(2)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
|
|
# delete all data
|
|
self.api['api-rule'].delete_item(
|
|
id2, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
|
|
self.api['api-rule'].delete_item(
|
|
id3, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
|
|
# insert data (which now DOES cause an execution)
|
|
(id4, rule3) = self.api['api-rule'].add_item(
|
|
{'rule': 'q(3)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
ans = "noargs"
|
|
f = lambda: nova.nova_client.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
# delete policy rule
|
|
self.api['api-rule'].delete_item(
|
|
id1, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 2)
|
|
|
|
def test_neutron_policy_execute(self):
|
|
class NeutronClient(object):
|
|
def __init__(self, testkey):
|
|
self.testkey = testkey
|
|
|
|
def disconnectNetwork(self, arg1):
|
|
LOG.info("disconnectNetwork called on %s", arg1)
|
|
self.testkey = "arg1=%s" % arg1
|
|
|
|
neutron_client = NeutronClient(None)
|
|
neutron = self.neutronv2
|
|
neutron.neutron = neutron_client
|
|
|
|
# insert rule and data
|
|
self.api['api-policy'].add_item({'name': 'alice'}, {})
|
|
(id1, _) = self.api['api-rule'].add_item(
|
|
{'rule': 'execute[neutron:disconnectNetwork(x)] :- q(x)'}, {},
|
|
context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 0)
|
|
(id2, _) = self.api['api-rule'].add_item(
|
|
{'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
|
|
self.assertEqual(len(self.engine.logger.messages), 1)
|
|
ans = "arg1=1"
|
|
f = lambda: neutron.neutron.testkey
|
|
helper.retry_check_function_return_value(f, ans)
|
|
|
|
def test_neutron_policy_poll_and_subscriptions(self):
|
|
"""Test polling and publishing of neutron updates."""
|
|
policy = self.engine.DEFAULT_THEORY
|
|
neutron2 = self._create_neutron_mock('neutron2')
|
|
self.engine.initialize_datasource('neutron',
|
|
self.neutronv2.get_schema())
|
|
self.engine.initialize_datasource('neutron2',
|
|
self.neutronv2.get_schema())
|
|
str_rule = ('p(x0, y0) :- neutron:networks(x0, x1, x2, x3, x4, x5), '
|
|
'neutron2:networks(y0, y1, y2, y3, y4, y5)')
|
|
rule = {'rule': str_rule, 'name': 'testrule1', 'comment': 'test'}
|
|
self.api['api-rule'].add_item(rule, {}, context={'policy_id': policy})
|
|
# Test policy subscriptions
|
|
subscriptions = self.engine.subscription_list()
|
|
self.assertEqual(sorted([('neutron', 'networks'),
|
|
('neutron2', 'networks')]), sorted(subscriptions))
|
|
# Test multiple instances
|
|
self.neutronv2.poll()
|
|
neutron2.poll()
|
|
ans = ('p("240ff9df-df35-43ae-9df5-27fae87f2492", '
|
|
' "240ff9df-df35-43ae-9df5-27fae87f2492") ')
|
|
helper.retry_check_db_equal(self.engine, 'p(x, y)', ans, target=policy)
|