octavia 14.0.0.0rc1__py3-none-any.whl → 14.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- octavia/amphorae/backends/agent/api_server/keepalivedlvs.py +9 -0
- octavia/amphorae/backends/agent/api_server/osutils.py +4 -2
- octavia/amphorae/backends/agent/api_server/plug.py +5 -4
- octavia/amphorae/backends/agent/api_server/server.py +3 -2
- octavia/amphorae/backends/agent/api_server/util.py +35 -2
- octavia/amphorae/backends/utils/interface.py +2 -37
- octavia/amphorae/backends/utils/interface_file.py +23 -10
- octavia/amphorae/backends/utils/nftable_utils.py +33 -11
- octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py +0 -1
- octavia/amphorae/drivers/keepalived/jinja/templates/keepalived_base.template +0 -1
- octavia/api/common/pagination.py +1 -1
- octavia/api/v2/controllers/health_monitor.py +3 -1
- octavia/api/v2/controllers/load_balancer.py +7 -0
- octavia/api/v2/controllers/member.py +12 -2
- octavia/common/clients.py +7 -1
- octavia/common/constants.py +3 -3
- octavia/controller/worker/v2/controller_worker.py +2 -2
- octavia/controller/worker/v2/flows/amphora_flows.py +14 -3
- octavia/controller/worker/v2/flows/flow_utils.py +6 -4
- octavia/controller/worker/v2/flows/listener_flows.py +17 -5
- octavia/controller/worker/v2/tasks/database_tasks.py +10 -6
- octavia/controller/worker/v2/tasks/network_tasks.py +27 -16
- octavia/db/base_models.py +16 -4
- octavia/network/drivers/neutron/allowed_address_pairs.py +3 -2
- octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +1 -1
- octavia/tests/functional/api/v2/test_health_monitor.py +18 -0
- octavia/tests/functional/api/v2/test_member.py +32 -0
- octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py +1 -1
- octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py +4 -3
- octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py +89 -1
- octavia/tests/unit/amphorae/backends/utils/test_interface.py +3 -64
- octavia/tests/unit/amphorae/backends/utils/test_nftable_utils.py +28 -22
- octavia/tests/unit/amphorae/drivers/keepalived/jinja/test_jinja_cfg.py +0 -4
- octavia/tests/unit/api/common/test_pagination.py +78 -1
- octavia/tests/unit/cmd/test_prometheus_proxy.py +8 -1
- octavia/tests/unit/controller/worker/v2/flows/test_listener_flows.py +10 -15
- octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +4 -6
- octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks.py +28 -6
- octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +71 -2
- octavia/tests/unit/controller/worker/v2/test_controller_worker.py +56 -1
- octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +2 -1
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/AUTHORS +5 -0
- octavia-14.0.2.dist-info/METADATA +156 -0
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/RECORD +59 -59
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/WHEEL +1 -1
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/entry_points.txt +0 -1
- octavia-14.0.2.dist-info/pbr.json +1 -0
- octavia-14.0.0.0rc1.dist-info/METADATA +0 -158
- octavia-14.0.0.0rc1.dist-info/pbr.json +0 -1
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/LICENSE +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/README.rst +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/README.rst +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/diskimage-create.sh +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/image-tests.sh +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/requirements.txt +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/test-requirements.txt +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/tox.ini +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/data/share/octavia/diskimage-create/version.txt +0 -0
- {octavia-14.0.0.0rc1.data → octavia-14.0.2.data}/scripts/octavia-wsgi +0 -0
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/LICENSE +0 -0
- {octavia-14.0.0.0rc1.dist-info → octavia-14.0.2.dist-info}/top_level.txt +0 -0
@@ -536,11 +536,10 @@ class PlugVIPAmphora(BaseNetworkTask):
|
|
536
536
|
"""Handle a failure to plumb a vip."""
|
537
537
|
if isinstance(result, failure.Failure):
|
538
538
|
return
|
539
|
+
lb_id = loadbalancer[constants.LOADBALANCER_ID]
|
539
540
|
LOG.warning("Unable to plug VIP for amphora id %s "
|
540
541
|
"load balancer id %s",
|
541
|
-
amphora.get(constants.ID),
|
542
|
-
loadbalancer[constants.LOADBALANCER_ID])
|
543
|
-
|
542
|
+
amphora.get(constants.ID), lb_id)
|
544
543
|
try:
|
545
544
|
session = db_apis.get_session()
|
546
545
|
with session.begin():
|
@@ -550,15 +549,16 @@ class PlugVIPAmphora(BaseNetworkTask):
|
|
550
549
|
db_amp.ha_port_id = result[constants.HA_PORT_ID]
|
551
550
|
db_subnet = self.network_driver.get_subnet(
|
552
551
|
subnet[constants.ID])
|
553
|
-
db_lb = self.loadbalancer_repo.get(
|
554
|
-
session,
|
555
|
-
id=loadbalancer[constants.LOADBALANCER_ID])
|
556
|
-
|
552
|
+
db_lb = self.loadbalancer_repo.get(session, id=lb_id)
|
557
553
|
self.network_driver.unplug_aap_port(db_lb.vip,
|
558
554
|
db_amp, db_subnet)
|
559
555
|
except Exception as e:
|
560
|
-
LOG.error(
|
561
|
-
|
556
|
+
LOG.error(
|
557
|
+
'Failed to unplug AAP port for load balancer: %s. '
|
558
|
+
'Resources may still be in use for VRRP port: %s. '
|
559
|
+
'Due to error: %s',
|
560
|
+
lb_id, result[constants.VRRP_PORT_ID], str(e)
|
561
|
+
)
|
562
562
|
|
563
563
|
|
564
564
|
class UnplugVIP(BaseNetworkTask):
|
@@ -706,7 +706,11 @@ class GetAmphoraNetworkConfigs(BaseNetworkTask):
|
|
706
706
|
db_lb, amphora=db_amp)
|
707
707
|
provider_dict = {}
|
708
708
|
for amp_id, amp_conf in db_configs.items():
|
709
|
-
|
709
|
+
# Do not serialize loadbalancer class. It's unused later and
|
710
|
+
# could be ignored for storing in results of task in persistence DB
|
711
|
+
provider_dict[amp_id] = amp_conf.to_dict(
|
712
|
+
recurse=True, calling_classes=[data_models.LoadBalancer]
|
713
|
+
)
|
710
714
|
return provider_dict
|
711
715
|
|
712
716
|
|
@@ -724,7 +728,11 @@ class GetAmphoraNetworkConfigsByID(BaseNetworkTask):
|
|
724
728
|
amphora=amphora)
|
725
729
|
provider_dict = {}
|
726
730
|
for amp_id, amp_conf in db_configs.items():
|
727
|
-
|
731
|
+
# Do not serialize loadbalancer class. It's unused later and
|
732
|
+
# could be ignored for storing in results of task in persistence DB
|
733
|
+
provider_dict[amp_id] = amp_conf.to_dict(
|
734
|
+
recurse=True, calling_classes=[data_models.LoadBalancer]
|
735
|
+
)
|
728
736
|
return provider_dict
|
729
737
|
|
730
738
|
|
@@ -740,7 +748,11 @@ class GetAmphoraeNetworkConfigs(BaseNetworkTask):
|
|
740
748
|
db_configs = self.network_driver.get_network_configs(db_lb)
|
741
749
|
provider_dict = {}
|
742
750
|
for amp_id, amp_conf in db_configs.items():
|
743
|
-
|
751
|
+
# Do not serialize loadbalancer class. It's unused later and
|
752
|
+
# could be ignored for storing in results of task in persistence DB
|
753
|
+
provider_dict[amp_id] = amp_conf.to_dict(
|
754
|
+
recurse=True, calling_classes=[data_models.LoadBalancer]
|
755
|
+
)
|
744
756
|
return provider_dict
|
745
757
|
|
746
758
|
|
@@ -984,10 +996,9 @@ class CreateVIPBasePort(BaseNetworkTask):
|
|
984
996
|
return
|
985
997
|
try:
|
986
998
|
port_name = constants.AMP_BASE_PORT_PREFIX + amphora_id
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
'revert.', port_name, port.id, amphora_id)
|
999
|
+
self.network_driver.delete_port(result[constants.ID])
|
1000
|
+
LOG.info('Deleted port %s with ID %s for amphora %s due to a '
|
1001
|
+
'revert.', port_name, result[constants.ID], amphora_id)
|
991
1002
|
except Exception as e:
|
992
1003
|
LOG.error('Failed to delete port %s. Resources may still be in '
|
993
1004
|
'use for a port intended for amphora %s due to error '
|
octavia/db/base_models.py
CHANGED
@@ -12,6 +12,8 @@
|
|
12
12
|
# License for the specific language governing permissions and limitations
|
13
13
|
# under the License.
|
14
14
|
|
15
|
+
from wsme import types as wtypes
|
16
|
+
|
15
17
|
from oslo_db.sqlalchemy import models
|
16
18
|
from oslo_utils import strutils
|
17
19
|
from oslo_utils import uuidutils
|
@@ -19,6 +21,8 @@ import sqlalchemy as sa
|
|
19
21
|
from sqlalchemy.orm import collections
|
20
22
|
from sqlalchemy.orm import declarative_base
|
21
23
|
|
24
|
+
from octavia.common import constants
|
25
|
+
|
22
26
|
|
23
27
|
class OctaviaBase(models.ModelBase):
|
24
28
|
|
@@ -112,12 +116,20 @@ class OctaviaBase(models.ModelBase):
|
|
112
116
|
|
113
117
|
@staticmethod
|
114
118
|
def apply_filter(query, model, filters):
|
119
|
+
# Convert boolean filters to proper type
|
120
|
+
for key in filters:
|
121
|
+
attr = getattr(model.__v2_wsme__, key, None)
|
122
|
+
if isinstance(attr, wtypes.wsattr) and attr.datatype == bool:
|
123
|
+
filters[key] = strutils.bool_from_string(filters[key])
|
124
|
+
# Special case for 'enabled', it's 'admin_state_up' in the WSME class
|
125
|
+
# definition and the attribute has already been renamed to 'enabled' by
|
126
|
+
# a previous pagination filter
|
127
|
+
if constants.ENABLED in filters:
|
128
|
+
filters[constants.ENABLED] = strutils.bool_from_string(
|
129
|
+
filters[constants.ENABLED])
|
130
|
+
|
115
131
|
translated_filters = {}
|
116
132
|
child_map = {}
|
117
|
-
# Convert enabled to proper type
|
118
|
-
if 'enabled' in filters:
|
119
|
-
filters['enabled'] = strutils.bool_from_string(
|
120
|
-
filters['enabled'])
|
121
133
|
for attr, name_map in model.__v2_wsme__._child_map.items():
|
122
134
|
for k, v in name_map.items():
|
123
135
|
if attr in filters and k in filters[attr]:
|
@@ -194,12 +194,13 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
|
|
194
194
|
# Don't remove egress rules and don't confuse other protocols with
|
195
195
|
# None ports with the egress rules. VRRP uses protocol 51 and 112
|
196
196
|
if (rule.get('direction') == 'egress' or
|
197
|
-
rule.get('protocol')
|
197
|
+
rule.get('protocol') is None or
|
198
|
+
rule['protocol'].upper() not in
|
198
199
|
[constants.PROTOCOL_TCP, constants.PROTOCOL_UDP,
|
199
200
|
lib_consts.PROTOCOL_SCTP]):
|
200
201
|
continue
|
201
202
|
old_ports.append((rule.get('port_range_max'),
|
202
|
-
rule
|
203
|
+
rule['protocol'].lower(),
|
203
204
|
rule.get('remote_ip_prefix')))
|
204
205
|
|
205
206
|
add_ports = set(updated_ports) - set(old_ports)
|
@@ -3060,7 +3060,7 @@ class TestServerTestCase(base.TestCase):
|
|
3060
3060
|
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
3061
3061
|
'load_nftables_file')
|
3062
3062
|
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
3063
|
-
'
|
3063
|
+
'write_nftable_rules_file')
|
3064
3064
|
@mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.'
|
3065
3065
|
'AmphoraInfo.get_interface')
|
3066
3066
|
def test_set_interface_rules(self, mock_get_int, mock_write_rules,
|
@@ -1782,6 +1782,24 @@ class TestHealthMonitor(base.BaseAPITest):
|
|
1782
1782
|
pool_prov_status=constants.PENDING_UPDATE,
|
1783
1783
|
hm_prov_status=constants.PENDING_UPDATE)
|
1784
1784
|
|
1785
|
+
def test_update_udp_case_with_udp_hm(self):
|
1786
|
+
api_hm = self.create_health_monitor(
|
1787
|
+
self.udp_pool_with_listener_id,
|
1788
|
+
constants.HEALTH_MONITOR_UDP_CONNECT, 3, 1, 1, 1).get(
|
1789
|
+
self.root_tag)
|
1790
|
+
self.set_lb_status(self.udp_lb_id)
|
1791
|
+
new_hm = {'timeout': 2}
|
1792
|
+
self.put(
|
1793
|
+
self.HM_PATH.format(healthmonitor_id=api_hm.get('id')),
|
1794
|
+
self._build_body(new_hm))
|
1795
|
+
self.assert_correct_status(
|
1796
|
+
lb_id=self.udp_lb_id, listener_id=self.udp_listener_id,
|
1797
|
+
pool_id=self.udp_pool_with_listener_id, hm_id=api_hm.get('id'),
|
1798
|
+
lb_prov_status=constants.PENDING_UPDATE,
|
1799
|
+
listener_prov_status=constants.PENDING_UPDATE,
|
1800
|
+
pool_prov_status=constants.PENDING_UPDATE,
|
1801
|
+
hm_prov_status=constants.PENDING_UPDATE)
|
1802
|
+
|
1785
1803
|
def test_negative_update_udp_case(self):
|
1786
1804
|
api_hm = self.create_health_monitor(
|
1787
1805
|
self.udp_pool_with_listener_id,
|
@@ -913,6 +913,38 @@ class TestMember(base.BaseAPITest):
|
|
913
913
|
m_subnet_exists.assert_called_once_with(
|
914
914
|
member1['subnet_id'], context=mock.ANY)
|
915
915
|
|
916
|
+
@mock.patch('octavia.api.drivers.driver_factory.get_driver')
|
917
|
+
@mock.patch('octavia.api.drivers.utils.call_provider')
|
918
|
+
def test_update_members_member_duplicate(
|
919
|
+
self, mock_provider, mock_get_driver):
|
920
|
+
mock_driver = mock.MagicMock()
|
921
|
+
mock_driver.name = 'noop_driver'
|
922
|
+
mock_get_driver.return_value = mock_driver
|
923
|
+
subnet_id = uuidutils.generate_uuid()
|
924
|
+
|
925
|
+
member1 = {'address': '192.0.2.1', 'protocol_port': 80,
|
926
|
+
'project_id': self.project_id, 'subnet_id': subnet_id}
|
927
|
+
|
928
|
+
req_dict = [member1]
|
929
|
+
body = {self.root_tag_list: req_dict}
|
930
|
+
path = self.MEMBERS_PATH.format(pool_id=self.pool_id)
|
931
|
+
self.put(path, body, status=202)
|
932
|
+
|
933
|
+
self.set_lb_status(self.lb_id)
|
934
|
+
|
935
|
+
# Same member (same address and protocol_port) updated twice in the
|
936
|
+
# same PUT request
|
937
|
+
member1 = {'address': '192.0.2.1', 'protocol_port': 80,
|
938
|
+
'project_id': self.project_id, 'subnet_id': subnet_id,
|
939
|
+
'name': 'member1'}
|
940
|
+
member2 = {'address': '192.0.2.1', 'protocol_port': 80,
|
941
|
+
'project_id': self.project_id, 'subnet_id': subnet_id,
|
942
|
+
'name': 'member2'}
|
943
|
+
|
944
|
+
req_dict = [member1, member2]
|
945
|
+
body = {self.root_tag_list: req_dict}
|
946
|
+
self.put(path, body, status=400)
|
947
|
+
|
916
948
|
@mock.patch('octavia.api.drivers.driver_factory.get_driver')
|
917
949
|
@mock.patch('octavia.api.drivers.utils.call_provider')
|
918
950
|
def test_update_members_subnet_not_found(
|
@@ -285,7 +285,7 @@ class TestPlug(base.TestCase):
|
|
285
285
|
self.test_plug.plug_network(FAKE_MAC_ADDRESS, fixed_ips, 1400)
|
286
286
|
|
287
287
|
mock_write_port_interface.assert_called_once_with(
|
288
|
-
interface='eth2', fixed_ips=fixed_ips, mtu=mtu)
|
288
|
+
interface='eth2', fixed_ips=fixed_ips, mtu=mtu, is_sriov=False)
|
289
289
|
mock_if_up.assert_called_once_with('eth2', 'network')
|
290
290
|
mock_send_member_adv.assert_called_once_with(fixed_ips)
|
291
291
|
|
@@ -332,7 +332,8 @@ class TestPlug(base.TestCase):
|
|
332
332
|
self.test_plug.plug_network(FAKE_MAC_ADDRESS, fixed_ips, 1400)
|
333
333
|
|
334
334
|
mock_write_port_interface.assert_called_once_with(
|
335
|
-
interface=FAKE_INTERFACE, fixed_ips=fixed_ips, mtu=mtu
|
335
|
+
interface=FAKE_INTERFACE, fixed_ips=fixed_ips, mtu=mtu,
|
336
|
+
is_sriov=False)
|
336
337
|
mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'network')
|
337
338
|
mock_send_member_adv.assert_called_once_with(fixed_ips)
|
338
339
|
|
@@ -401,7 +402,7 @@ class TestPlug(base.TestCase):
|
|
401
402
|
'gateway': vip_net_info['gateway'],
|
402
403
|
'host_routes': [],
|
403
404
|
},
|
404
|
-
fixed_ips=fixed_ips, mtu=mtu)
|
405
|
+
fixed_ips=fixed_ips, mtu=mtu, is_sriov=False)
|
405
406
|
|
406
407
|
mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'vip')
|
407
408
|
mock_send_member_adv.assert_called_once_with(fixed_ips)
|
@@ -370,13 +370,57 @@ class TestUtil(base.TestCase):
|
|
370
370
|
self.assertEqual([], util.get_haproxy_vip_addresses(LB_ID1))
|
371
371
|
mock_cfg_path.assert_called_once_with(LB_ID1)
|
372
372
|
|
373
|
+
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
|
374
|
+
'keepalived_lvs_cfg_path')
|
375
|
+
def test_get_lvs_vip_addresses(self, mock_cfg_path):
|
376
|
+
FAKE_PATH = 'fake_path'
|
377
|
+
mock_cfg_path.return_value = FAKE_PATH
|
378
|
+
self.useFixture(
|
379
|
+
test_utils.OpenFixture(FAKE_PATH, 'no match')).mock_open()
|
380
|
+
|
381
|
+
# Test with no matching lines in the config file
|
382
|
+
self.assertEqual([], util.get_lvs_vip_addresses(LB_ID1))
|
383
|
+
mock_cfg_path.assert_called_once_with(LB_ID1)
|
384
|
+
|
385
|
+
# Test with 2 matching lines
|
386
|
+
mock_cfg_path.reset_mock()
|
387
|
+
test_data = ('virtual_server_group ipv4-group {\n'
|
388
|
+
' 203.0.113.43 1\n'
|
389
|
+
' 203.0.113.44 1\n'
|
390
|
+
'}\n')
|
391
|
+
self.useFixture(
|
392
|
+
test_utils.OpenFixture(FAKE_PATH, test_data)).mock_open()
|
393
|
+
expected_result = ['203.0.113.43', '203.0.113.44']
|
394
|
+
self.assertEqual(expected_result,
|
395
|
+
util.get_lvs_vip_addresses(LB_ID1))
|
396
|
+
mock_cfg_path.assert_called_once_with(LB_ID1)
|
397
|
+
|
398
|
+
# Test with 2 groups
|
399
|
+
mock_cfg_path.reset_mock()
|
400
|
+
test_data = ('virtual_server_group ipv4-group {\n'
|
401
|
+
' 203.0.113.43 1\n'
|
402
|
+
'}\n'
|
403
|
+
'virtual_server_group ipv6-group {\n'
|
404
|
+
' 2d01:27::1 2\n'
|
405
|
+
' 2d01:27::2 2\n'
|
406
|
+
'}\n')
|
407
|
+
self.useFixture(
|
408
|
+
test_utils.OpenFixture(FAKE_PATH, test_data)).mock_open()
|
409
|
+
expected_result = ['203.0.113.43', '2d01:27::1', '2d01:27::2']
|
410
|
+
self.assertEqual(expected_result,
|
411
|
+
util.get_lvs_vip_addresses(LB_ID1))
|
412
|
+
mock_cfg_path.assert_called_once_with(LB_ID1)
|
413
|
+
|
373
414
|
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
|
374
415
|
'send_ip_advertisement')
|
375
416
|
@mock.patch('octavia.amphorae.backends.utils.network_utils.'
|
376
417
|
'get_interface_name')
|
377
418
|
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
|
378
419
|
'get_haproxy_vip_addresses')
|
379
|
-
|
420
|
+
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
|
421
|
+
'get_lvs_vip_addresses')
|
422
|
+
def test_send_vip_advertisements(self, mock_get_lvs_vip_addrs,
|
423
|
+
mock_get_vip_addrs,
|
380
424
|
mock_get_int_name, mock_send_advert):
|
381
425
|
mock_get_vip_addrs.side_effect = [[], ['203.0.113.46'],
|
382
426
|
Exception('boom')]
|
@@ -385,6 +429,7 @@ class TestUtil(base.TestCase):
|
|
385
429
|
# Test no VIPs
|
386
430
|
util.send_vip_advertisements(LB_ID1)
|
387
431
|
mock_get_vip_addrs.assert_called_once_with(LB_ID1)
|
432
|
+
mock_get_lvs_vip_addrs.assert_not_called()
|
388
433
|
mock_get_int_name.assert_not_called()
|
389
434
|
mock_send_advert.assert_not_called()
|
390
435
|
|
@@ -394,6 +439,7 @@ class TestUtil(base.TestCase):
|
|
394
439
|
mock_send_advert.reset_mock()
|
395
440
|
util.send_vip_advertisements(LB_ID1)
|
396
441
|
mock_get_vip_addrs.assert_called_once_with(LB_ID1)
|
442
|
+
mock_get_lvs_vip_addrs.assert_not_called()
|
397
443
|
mock_get_int_name.assert_called_once_with(
|
398
444
|
'203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
|
399
445
|
mock_send_advert.assert_called_once_with(
|
@@ -407,6 +453,48 @@ class TestUtil(base.TestCase):
|
|
407
453
|
mock_get_int_name.assert_not_called()
|
408
454
|
mock_send_advert.assert_not_called()
|
409
455
|
|
456
|
+
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
|
457
|
+
'send_ip_advertisement')
|
458
|
+
@mock.patch('octavia.amphorae.backends.utils.network_utils.'
|
459
|
+
'get_interface_name')
|
460
|
+
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
|
461
|
+
'get_haproxy_vip_addresses')
|
462
|
+
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
|
463
|
+
'get_lvs_vip_addresses')
|
464
|
+
def test_send_vip_advertisements_udp(self, mock_get_lvs_vip_addrs,
|
465
|
+
mock_get_vip_addrs,
|
466
|
+
mock_get_int_name, mock_send_advert):
|
467
|
+
mock_get_lvs_vip_addrs.side_effect = [[], ['203.0.113.46'],
|
468
|
+
Exception('boom')]
|
469
|
+
mock_get_int_name.return_value = 'fake0'
|
470
|
+
|
471
|
+
# Test no VIPs
|
472
|
+
util.send_vip_advertisements(listener_id=LISTENER_ID1)
|
473
|
+
mock_get_lvs_vip_addrs.assert_called_once_with(LISTENER_ID1)
|
474
|
+
mock_get_vip_addrs.assert_not_called()
|
475
|
+
mock_get_int_name.assert_not_called()
|
476
|
+
mock_send_advert.assert_not_called()
|
477
|
+
|
478
|
+
# Test with a VIP
|
479
|
+
mock_get_lvs_vip_addrs.reset_mock()
|
480
|
+
mock_get_int_name.reset_mock()
|
481
|
+
mock_send_advert.reset_mock()
|
482
|
+
util.send_vip_advertisements(listener_id=LISTENER_ID1)
|
483
|
+
mock_get_lvs_vip_addrs.assert_called_once_with(LISTENER_ID1)
|
484
|
+
mock_get_vip_addrs.assert_not_called()
|
485
|
+
mock_get_int_name.assert_called_once_with(
|
486
|
+
'203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
|
487
|
+
mock_send_advert.assert_called_once_with(
|
488
|
+
'fake0', '203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
|
489
|
+
|
490
|
+
# Test with an exception (should not raise)
|
491
|
+
mock_get_lvs_vip_addrs.reset_mock()
|
492
|
+
mock_get_int_name.reset_mock()
|
493
|
+
mock_send_advert.reset_mock()
|
494
|
+
util.send_vip_advertisements(listener_id=LISTENER_ID1)
|
495
|
+
mock_get_int_name.assert_not_called()
|
496
|
+
mock_send_advert.assert_not_called()
|
497
|
+
|
410
498
|
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
|
411
499
|
'send_ip_advertisement')
|
412
500
|
@mock.patch('octavia.amphorae.backends.utils.network_utils.'
|
@@ -452,7 +452,7 @@ class TestInterface(base.TestCase):
|
|
452
452
|
@mock.patch('octavia.amphorae.backends.utils.network_namespace.'
|
453
453
|
'NetworkNamespace')
|
454
454
|
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
455
|
-
'
|
455
|
+
'write_nftable_rules_file')
|
456
456
|
@mock.patch('pyroute2.IPRoute.rule')
|
457
457
|
@mock.patch('pyroute2.IPRoute.route')
|
458
458
|
@mock.patch('pyroute2.IPRoute.addr')
|
@@ -578,16 +578,8 @@ class TestInterface(base.TestCase):
|
|
578
578
|
family=socket.AF_INET6)])
|
579
579
|
|
580
580
|
mock_check_output.assert_has_calls([
|
581
|
-
mock.call([consts.NFT_CMD,
|
582
|
-
|
583
|
-
mock.call([consts.NFT_CMD, consts.NFT_ADD, 'chain',
|
584
|
-
consts.NFT_FAMILY, consts.NFT_VIP_TABLE,
|
585
|
-
consts.NFT_VIP_CHAIN, '{', 'type', 'filter', 'hook',
|
586
|
-
'ingress', 'device', 'fake-eth1', 'priority',
|
587
|
-
consts.NFT_SRIOV_PRIORITY, ';', 'policy', 'drop', ';',
|
588
|
-
'}'], stderr=-2),
|
589
|
-
mock.call([consts.NFT_CMD, '-o', '-f', consts.NFT_VIP_RULES_FILE],
|
590
|
-
stderr=-2),
|
581
|
+
mock.call([consts.NFT_CMD, '-o', '-f', consts.NFT_RULES_FILE],
|
582
|
+
stderr=subprocess.STDOUT),
|
591
583
|
mock.call(["post-up", "fake-eth1"])
|
592
584
|
])
|
593
585
|
|
@@ -1444,56 +1436,3 @@ class TestInterface(base.TestCase):
|
|
1444
1436
|
|
1445
1437
|
addr = controller._normalize_ip_network(None)
|
1446
1438
|
self.assertIsNone(addr)
|
1447
|
-
|
1448
|
-
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
1449
|
-
'load_nftables_file')
|
1450
|
-
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
1451
|
-
'write_nftable_vip_rules_file')
|
1452
|
-
@mock.patch('subprocess.check_output')
|
1453
|
-
def test__setup_nftables_chain(self, mock_check_output, mock_write_rules,
|
1454
|
-
mock_load_rules):
|
1455
|
-
|
1456
|
-
controller = interface.InterfaceController()
|
1457
|
-
|
1458
|
-
mock_check_output.side_effect = [
|
1459
|
-
mock.DEFAULT, mock.DEFAULT,
|
1460
|
-
subprocess.CalledProcessError(cmd=consts.NFT_CMD, returncode=-1),
|
1461
|
-
mock.DEFAULT,
|
1462
|
-
subprocess.CalledProcessError(cmd=consts.NFT_CMD, returncode=-1)]
|
1463
|
-
|
1464
|
-
interface_mock = mock.MagicMock()
|
1465
|
-
interface_mock.name = 'fake2'
|
1466
|
-
|
1467
|
-
# Test succeessful path
|
1468
|
-
controller._setup_nftables_chain(interface_mock)
|
1469
|
-
|
1470
|
-
mock_write_rules.assert_called_once_with('fake2', [])
|
1471
|
-
mock_load_rules.assert_called_once_with()
|
1472
|
-
mock_check_output.assert_has_calls([
|
1473
|
-
mock.call([consts.NFT_CMD, 'add', 'table', consts.NFT_FAMILY,
|
1474
|
-
consts.NFT_VIP_TABLE], stderr=subprocess.STDOUT),
|
1475
|
-
mock.call([consts.NFT_CMD, 'add', 'chain', consts.NFT_FAMILY,
|
1476
|
-
consts.NFT_VIP_TABLE, consts.NFT_VIP_CHAIN, '{',
|
1477
|
-
'type', 'filter', 'hook', 'ingress', 'device',
|
1478
|
-
'fake2', 'priority', consts.NFT_SRIOV_PRIORITY, ';',
|
1479
|
-
'policy', 'drop', ';', '}'], stderr=subprocess.STDOUT)])
|
1480
|
-
|
1481
|
-
# Test first nft call fails
|
1482
|
-
mock_write_rules.reset_mock()
|
1483
|
-
mock_load_rules.reset_mock()
|
1484
|
-
mock_check_output.reset_mock()
|
1485
|
-
|
1486
|
-
self.assertRaises(subprocess.CalledProcessError,
|
1487
|
-
controller._setup_nftables_chain, interface_mock)
|
1488
|
-
mock_check_output.assert_called_once()
|
1489
|
-
mock_write_rules.assert_not_called()
|
1490
|
-
|
1491
|
-
# Test second nft call fails
|
1492
|
-
mock_write_rules.reset_mock()
|
1493
|
-
mock_load_rules.reset_mock()
|
1494
|
-
mock_check_output.reset_mock()
|
1495
|
-
|
1496
|
-
self.assertRaises(subprocess.CalledProcessError,
|
1497
|
-
controller._setup_nftables_chain, interface_mock)
|
1498
|
-
self.assertEqual(2, mock_check_output.call_count)
|
1499
|
-
mock_write_rules.assert_not_called()
|
@@ -28,23 +28,22 @@ import octavia.tests.unit.base as base
|
|
28
28
|
class TestNFTableUtils(base.TestCase):
|
29
29
|
@mock.patch('os.open')
|
30
30
|
@mock.patch('os.path.isfile')
|
31
|
-
def
|
31
|
+
def test_write_nftable_rules_file_exists(self, mock_isfile, mock_open):
|
32
32
|
"""Test when a rules file exists and no new rules
|
33
33
|
|
34
34
|
When an existing rules file is present and we call
|
35
|
-
|
35
|
+
write_nftable_rules_file with no rules, the method should not
|
36
36
|
overwrite the existing rules.
|
37
37
|
"""
|
38
38
|
mock_isfile.return_value = True
|
39
39
|
|
40
|
-
nftable_utils.
|
40
|
+
nftable_utils.write_nftable_rules_file('fake-eth2', [])
|
41
41
|
|
42
42
|
mock_open.assert_not_called()
|
43
43
|
|
44
44
|
@mock.patch('os.open')
|
45
45
|
@mock.patch('os.path.isfile')
|
46
|
-
def
|
47
|
-
mock_open):
|
46
|
+
def test_write_nftable_rules_file_rules(self, mock_isfile, mock_open):
|
48
47
|
"""Test when a rules file exists and rules are passed in
|
49
48
|
|
50
49
|
This should create a simple rules file with the base chain and rules.
|
@@ -61,32 +60,40 @@ class TestNFTableUtils(base.TestCase):
|
|
61
60
|
|
62
61
|
mocked_open = mock.mock_open()
|
63
62
|
with mock.patch.object(os, 'fdopen', mocked_open):
|
64
|
-
nftable_utils.
|
63
|
+
nftable_utils.write_nftable_rules_file(
|
65
64
|
'fake-eth2', [test_rule_1, test_rule_2])
|
66
65
|
|
67
66
|
mocked_open.assert_called_once_with('fake-fd', 'w')
|
68
67
|
mock_open.assert_called_once_with(
|
69
|
-
consts.
|
68
|
+
consts.NFT_RULES_FILE,
|
70
69
|
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
71
70
|
(stat.S_IRUSR | stat.S_IWUSR))
|
72
71
|
|
73
72
|
handle = mocked_open()
|
74
73
|
handle.write.assert_has_calls([
|
75
|
-
mock.call(f'table {consts.NFT_FAMILY} {consts.
|
74
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_TABLE} '
|
76
75
|
'{}\n'),
|
77
76
|
mock.call(f'delete table {consts.NFT_FAMILY} '
|
78
|
-
f'{consts.
|
79
|
-
mock.call(f'table {consts.NFT_FAMILY} {consts.
|
77
|
+
f'{consts.NFT_TABLE}\n'),
|
78
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_TABLE} '
|
80
79
|
'{\n'),
|
81
|
-
mock.call(f' chain {consts.
|
82
|
-
mock.call(' type filter hook
|
83
|
-
|
80
|
+
mock.call(f' chain {consts.NFT_CHAIN} {{\n'),
|
81
|
+
mock.call(' type filter hook input priority filter; '
|
82
|
+
'policy drop;\n'),
|
83
|
+
mock.call(' ct state vmap { established : accept, related : '
|
84
|
+
'accept, invalid : drop }\n'),
|
85
|
+
mock.call(' iif lo accept\n'),
|
86
|
+
mock.call(' ip saddr 127.0.0.0/8 drop\n'),
|
87
|
+
mock.call(' ip6 saddr ::1 drop\n'),
|
84
88
|
mock.call(' icmp type destination-unreachable accept\n'),
|
85
89
|
mock.call(' icmpv6 type { nd-neighbor-solicit, '
|
86
90
|
'nd-router-advert, nd-neighbor-advert, packet-too-big, '
|
87
91
|
'destination-unreachable } accept\n'),
|
88
92
|
mock.call(' udp sport 67 udp dport 68 accept\n'),
|
89
93
|
mock.call(' udp sport 547 udp dport 546 accept\n'),
|
94
|
+
mock.call(' iifname eth1 goto amphora_vip_chain\n'),
|
95
|
+
mock.call(' }\n'),
|
96
|
+
mock.call(' chain amphora_vip_chain {\n'),
|
90
97
|
mock.call(' tcp dport 1234 accept\n'),
|
91
98
|
mock.call(' ip saddr 192.0.2.0/24 ip protocol 112 accept\n'),
|
92
99
|
mock.call(' }\n'),
|
@@ -95,8 +102,7 @@ class TestNFTableUtils(base.TestCase):
|
|
95
102
|
|
96
103
|
@mock.patch('os.open')
|
97
104
|
@mock.patch('os.path.isfile')
|
98
|
-
def
|
99
|
-
mock_open):
|
105
|
+
def test_write_nftable_rules_file_missing(self, mock_isfile, mock_open):
|
100
106
|
"""Test when a rules file does not exist and no new rules
|
101
107
|
|
102
108
|
This should create a simple rules file with the base chain.
|
@@ -106,21 +112,21 @@ class TestNFTableUtils(base.TestCase):
|
|
106
112
|
|
107
113
|
mocked_open = mock.mock_open()
|
108
114
|
with mock.patch.object(os, 'fdopen', mocked_open):
|
109
|
-
nftable_utils.
|
115
|
+
nftable_utils.write_nftable_rules_file('fake-eth2', [])
|
110
116
|
|
111
117
|
mocked_open.assert_called_once_with('fake-fd', 'w')
|
112
118
|
mock_open.assert_called_once_with(
|
113
|
-
consts.
|
119
|
+
consts.NFT_RULES_FILE,
|
114
120
|
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
115
121
|
(stat.S_IRUSR | stat.S_IWUSR))
|
116
122
|
|
117
123
|
handle = mocked_open()
|
118
124
|
handle.write.assert_has_calls([
|
119
|
-
mock.call(f'table {consts.NFT_FAMILY} {consts.
|
125
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_TABLE} '
|
120
126
|
'{\n'),
|
121
|
-
mock.call(f' chain {consts.
|
122
|
-
mock.call(' type filter hook
|
123
|
-
|
127
|
+
mock.call(f' chain {consts.NFT_CHAIN} {{\n'),
|
128
|
+
mock.call(' type filter hook input priority filter; '
|
129
|
+
'policy drop;\n'),
|
124
130
|
mock.call(' icmp type destination-unreachable accept\n'),
|
125
131
|
mock.call(' icmpv6 type { nd-neighbor-solicit, '
|
126
132
|
'nd-router-advert, nd-neighbor-advert, packet-too-big, '
|
@@ -184,7 +190,7 @@ class TestNFTableUtils(base.TestCase):
|
|
184
190
|
|
185
191
|
mock_netns.assert_called_once_with(consts.AMPHORA_NAMESPACE)
|
186
192
|
mock_check_output.assert_called_once_with([
|
187
|
-
consts.NFT_CMD, '-o', '-f', consts.
|
193
|
+
consts.NFT_CMD, '-o', '-f', consts.NFT_RULES_FILE],
|
188
194
|
stderr=subprocess.STDOUT)
|
189
195
|
|
190
196
|
self.assertRaises(subprocess.CalledProcessError,
|
@@ -71,7 +71,6 @@ class TestVRRPRestDriver(base.TestCase):
|
|
71
71
|
"}\n"
|
72
72
|
"\n"
|
73
73
|
"vrrp_instance TESTGROUP {\n"
|
74
|
-
" state MASTER\n"
|
75
74
|
" interface eth1\n"
|
76
75
|
" virtual_router_id 1\n"
|
77
76
|
" priority 100\n"
|
@@ -124,7 +123,6 @@ class TestVRRPRestDriver(base.TestCase):
|
|
124
123
|
"}\n"
|
125
124
|
"\n"
|
126
125
|
"vrrp_instance TESTGROUP {\n"
|
127
|
-
" state MASTER\n"
|
128
126
|
" interface eth1\n"
|
129
127
|
" virtual_router_id 1\n"
|
130
128
|
" priority 100\n"
|
@@ -170,7 +168,6 @@ class TestVRRPRestDriver(base.TestCase):
|
|
170
168
|
"}\n"
|
171
169
|
"\n"
|
172
170
|
"vrrp_instance TESTGROUP {\n"
|
173
|
-
" state MASTER\n"
|
174
171
|
" interface eth1\n"
|
175
172
|
" virtual_router_id 1\n"
|
176
173
|
" priority 100\n"
|
@@ -220,7 +217,6 @@ class TestVRRPRestDriver(base.TestCase):
|
|
220
217
|
"}\n"
|
221
218
|
"\n"
|
222
219
|
"vrrp_instance TESTGROUP {\n"
|
223
|
-
" state MASTER\n"
|
224
220
|
" interface eth1\n"
|
225
221
|
" virtual_router_id 1\n"
|
226
222
|
" priority 100\n"
|