477 lines
18 KiB
Python
477 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# 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.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.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()
|
|
|
|
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)
|
|
|
|
|
|
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)
|