congress/congress/tests/test_congress.py

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)