octavia 13.0.0.0rc1__py3-none-any.whl → 14.0.0__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/lvs_listener_base.py +1 -1
- octavia/amphorae/backends/agent/api_server/osutils.py +5 -5
- octavia/amphorae/backends/agent/api_server/plug.py +3 -2
- octavia/amphorae/backends/agent/api_server/rules_schema.py +52 -0
- octavia/amphorae/backends/agent/api_server/server.py +28 -1
- octavia/amphorae/backends/utils/interface.py +45 -6
- octavia/amphorae/backends/utils/interface_file.py +9 -6
- octavia/amphorae/backends/utils/nftable_utils.py +125 -0
- octavia/amphorae/drivers/driver_base.py +27 -0
- octavia/amphorae/drivers/haproxy/rest_api_driver.py +42 -10
- octavia/amphorae/drivers/health/heartbeat_udp.py +2 -2
- octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py +2 -1
- octavia/amphorae/drivers/noop_driver/driver.py +25 -0
- octavia/api/app.py +3 -0
- octavia/api/common/pagination.py +2 -2
- octavia/api/drivers/amphora_driver/flavor_schema.py +6 -1
- octavia/api/root_controller.py +4 -1
- octavia/api/v2/controllers/health_monitor.py +0 -1
- octavia/api/v2/controllers/l7policy.py +0 -1
- octavia/api/v2/controllers/l7rule.py +0 -1
- octavia/api/v2/controllers/listener.py +0 -1
- octavia/api/v2/controllers/load_balancer.py +13 -7
- octavia/api/v2/controllers/member.py +6 -3
- octavia/api/v2/controllers/pool.py +6 -7
- octavia/api/v2/types/load_balancer.py +5 -1
- octavia/api/v2/types/pool.py +1 -1
- octavia/certificates/common/pkcs12.py +9 -9
- octavia/certificates/manager/barbican.py +24 -16
- octavia/certificates/manager/castellan_mgr.py +12 -7
- octavia/certificates/manager/local.py +4 -4
- octavia/certificates/manager/noop.py +106 -0
- octavia/cmd/driver_agent.py +1 -1
- octavia/cmd/health_checker.py +0 -4
- octavia/cmd/health_manager.py +1 -5
- octavia/cmd/house_keeping.py +1 -1
- octavia/cmd/interface.py +0 -4
- octavia/cmd/octavia_worker.py +0 -4
- octavia/cmd/prometheus_proxy.py +0 -5
- octavia/cmd/status.py +0 -6
- octavia/common/base_taskflow.py +1 -1
- octavia/common/clients.py +15 -3
- octavia/common/config.py +24 -6
- octavia/common/constants.py +34 -0
- octavia/common/data_models.py +3 -1
- octavia/common/exceptions.py +11 -0
- octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 +7 -5
- octavia/common/keystone.py +7 -7
- octavia/common/tls_utils/cert_parser.py +24 -10
- octavia/common/utils.py +6 -0
- octavia/common/validate.py +2 -2
- octavia/compute/drivers/nova_driver.py +23 -5
- octavia/controller/worker/task_utils.py +28 -6
- octavia/controller/worker/v2/controller_worker.py +49 -15
- octavia/controller/worker/v2/flows/amphora_flows.py +120 -21
- octavia/controller/worker/v2/flows/flow_utils.py +15 -13
- octavia/controller/worker/v2/flows/listener_flows.py +95 -5
- octavia/controller/worker/v2/flows/load_balancer_flows.py +74 -30
- octavia/controller/worker/v2/taskflow_jobboard_driver.py +17 -1
- octavia/controller/worker/v2/tasks/amphora_driver_tasks.py +145 -24
- octavia/controller/worker/v2/tasks/compute_tasks.py +1 -1
- octavia/controller/worker/v2/tasks/database_tasks.py +72 -41
- octavia/controller/worker/v2/tasks/lifecycle_tasks.py +97 -41
- octavia/controller/worker/v2/tasks/network_tasks.py +57 -60
- octavia/controller/worker/v2/tasks/shim_tasks.py +28 -0
- octavia/db/migration/alembic_migrations/versions/55874a4ceed6_add_l7policy_action_redirect_prefix.py +1 -1
- octavia/db/migration/alembic_migrations/versions/5a3ee5472c31_add_cert_expiration__infor_in_amphora_table.py +1 -1
- octavia/db/migration/alembic_migrations/versions/6742ca1b27c2_add_l7policy_redirect_http_code.py +1 -1
- octavia/db/migration/alembic_migrations/versions/db2a73e82626_add_vnic_type_for_vip.py +36 -0
- octavia/db/models.py +1 -0
- octavia/db/prepare.py +1 -1
- octavia/db/repositories.py +53 -34
- octavia/distributor/drivers/driver_base.py +1 -1
- octavia/network/base.py +3 -16
- octavia/network/data_models.py +4 -1
- octavia/network/drivers/neutron/allowed_address_pairs.py +27 -26
- octavia/network/drivers/noop_driver/driver.py +10 -23
- octavia/tests/common/sample_certs.py +115 -0
- octavia/tests/common/sample_haproxy_prometheus +1 -1
- octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +37 -0
- octavia/tests/functional/api/test_healthcheck.py +2 -2
- octavia/tests/functional/api/v2/base.py +1 -1
- octavia/tests/functional/api/v2/test_listener.py +45 -0
- octavia/tests/functional/api/v2/test_load_balancer.py +17 -0
- octavia/tests/functional/db/base.py +9 -0
- octavia/tests/functional/db/test_models.py +2 -1
- octavia/tests/functional/db/test_repositories.py +55 -99
- octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py +4 -2
- octavia/tests/unit/amphorae/backends/utils/test_interface.py +201 -1
- octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py +1 -1
- octavia/tests/unit/amphorae/backends/utils/test_nftable_utils.py +194 -0
- octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +27 -5
- octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py +15 -2
- octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py +17 -0
- octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py +2 -1
- octavia/tests/unit/api/v2/types/test_pool.py +71 -0
- octavia/tests/unit/certificates/manager/test_barbican.py +3 -3
- octavia/tests/unit/certificates/manager/test_noop.py +53 -0
- octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py +16 -17
- octavia/tests/unit/common/sample_configs/sample_configs_combined.py +5 -3
- octavia/tests/unit/common/test_config.py +35 -0
- octavia/tests/unit/common/test_keystone.py +32 -0
- octavia/tests/unit/common/test_utils.py +39 -0
- octavia/tests/unit/compute/drivers/test_nova_driver.py +22 -0
- octavia/tests/unit/controller/worker/test_task_utils.py +58 -2
- octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py +28 -5
- octavia/tests/unit/controller/worker/v2/flows/test_listener_flows.py +64 -16
- octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +49 -9
- octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py +265 -17
- octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks.py +101 -1
- octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py +19 -19
- octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +105 -42
- octavia/tests/unit/controller/worker/v2/tasks/test_shim_tasks.py +33 -0
- octavia/tests/unit/controller/worker/v2/test_controller_worker.py +85 -42
- octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +48 -51
- octavia/tests/unit/network/drivers/neutron/test_utils.py +2 -0
- octavia/tests/unit/network/drivers/noop_driver/test_driver.py +0 -7
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/README.rst +6 -1
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/diskimage-create.sh +10 -4
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/requirements.txt +0 -2
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/tox.ini +30 -13
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/AUTHORS +5 -0
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/METADATA +6 -6
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/RECORD +134 -126
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/entry_points.txt +1 -1
- octavia-14.0.0.dist-info/pbr.json +1 -0
- octavia-13.0.0.0rc1.dist-info/pbr.json +0 -1
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/LICENSE +0 -0
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/README.rst +0 -0
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/image-tests.sh +0 -0
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/test-requirements.txt +0 -0
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/version.txt +0 -0
- {octavia-13.0.0.0rc1.data → octavia-14.0.0.data}/scripts/octavia-wsgi +0 -0
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/LICENSE +0 -0
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/WHEEL +0 -0
- {octavia-13.0.0.0rc1.dist-info → octavia-14.0.0.dist-info}/top_level.txt +0 -0
@@ -163,7 +163,8 @@ class TestOSUtils(base.TestCase):
|
|
163
163
|
mtu=MTU,
|
164
164
|
vrrp_info=None,
|
165
165
|
fixed_ips=None,
|
166
|
-
topology="SINGLE"
|
166
|
+
topology="SINGLE",
|
167
|
+
is_sriov=False)
|
167
168
|
mock_vip_interface_file.return_value.write.assert_called_once()
|
168
169
|
|
169
170
|
# Now test with an IPv6 VIP
|
@@ -193,7 +194,8 @@ class TestOSUtils(base.TestCase):
|
|
193
194
|
mtu=MTU,
|
194
195
|
vrrp_info=None,
|
195
196
|
fixed_ips=None,
|
196
|
-
topology="SINGLE"
|
197
|
+
topology="SINGLE",
|
198
|
+
is_sriov=False)
|
197
199
|
|
198
200
|
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
|
199
201
|
'PortInterfaceFile')
|
@@ -15,6 +15,7 @@
|
|
15
15
|
import errno
|
16
16
|
import os
|
17
17
|
import socket
|
18
|
+
import subprocess
|
18
19
|
from unittest import mock
|
19
20
|
|
20
21
|
import pyroute2
|
@@ -448,6 +449,150 @@ class TestInterface(base.TestCase):
|
|
448
449
|
mock.call(["post-up", "eth1"])
|
449
450
|
])
|
450
451
|
|
452
|
+
@mock.patch('octavia.amphorae.backends.utils.network_namespace.'
|
453
|
+
'NetworkNamespace')
|
454
|
+
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
455
|
+
'write_nftable_vip_rules_file')
|
456
|
+
@mock.patch('pyroute2.IPRoute.rule')
|
457
|
+
@mock.patch('pyroute2.IPRoute.route')
|
458
|
+
@mock.patch('pyroute2.IPRoute.addr')
|
459
|
+
@mock.patch('pyroute2.IPRoute.link')
|
460
|
+
@mock.patch('pyroute2.IPRoute.get_links')
|
461
|
+
@mock.patch('pyroute2.IPRoute.link_lookup')
|
462
|
+
@mock.patch('subprocess.check_output')
|
463
|
+
def test_up_sriov(self, mock_check_output, mock_link_lookup,
|
464
|
+
mock_get_links, mock_link, mock_addr, mock_route,
|
465
|
+
mock_rule, mock_nftable, mock_netns):
|
466
|
+
iface = interface_file.InterfaceFile(
|
467
|
+
name="fake-eth1",
|
468
|
+
if_type="vip",
|
469
|
+
mtu=1450,
|
470
|
+
addresses=[{
|
471
|
+
consts.ADDRESS: '192.0.2.4',
|
472
|
+
consts.PREFIXLEN: 24
|
473
|
+
}, {
|
474
|
+
consts.ADDRESS: '198.51.100.4',
|
475
|
+
consts.PREFIXLEN: 16
|
476
|
+
}, {
|
477
|
+
consts.ADDRESS: '2001:db8::3',
|
478
|
+
consts.PREFIXLEN: 64
|
479
|
+
}],
|
480
|
+
routes=[{
|
481
|
+
consts.DST: '203.0.113.0/24',
|
482
|
+
consts.GATEWAY: '192.0.2.1',
|
483
|
+
consts.TABLE: 10,
|
484
|
+
consts.ONLINK: True
|
485
|
+
}, {
|
486
|
+
consts.DST: '198.51.100.0/24',
|
487
|
+
consts.GATEWAY: '192.0.2.2',
|
488
|
+
consts.PREFSRC: '192.0.2.4',
|
489
|
+
consts.SCOPE: 'link'
|
490
|
+
}, {
|
491
|
+
consts.DST: '2001:db8:2::1/128',
|
492
|
+
consts.GATEWAY: '2001:db8::1'
|
493
|
+
}],
|
494
|
+
rules=[{
|
495
|
+
consts.SRC: '203.0.113.1',
|
496
|
+
consts.SRC_LEN: 32,
|
497
|
+
consts.TABLE: 20,
|
498
|
+
}, {
|
499
|
+
consts.SRC: '2001:db8::1',
|
500
|
+
consts.SRC_LEN: 128,
|
501
|
+
consts.TABLE: 40,
|
502
|
+
}],
|
503
|
+
scripts={
|
504
|
+
consts.IFACE_UP: [{
|
505
|
+
consts.COMMAND: "post-up fake-eth1"
|
506
|
+
}],
|
507
|
+
consts.IFACE_DOWN: [{
|
508
|
+
consts.COMMAND: "post-down fake-eth1"
|
509
|
+
}],
|
510
|
+
},
|
511
|
+
is_sriov=True)
|
512
|
+
|
513
|
+
idx = mock.MagicMock()
|
514
|
+
mock_link_lookup.return_value = [idx]
|
515
|
+
|
516
|
+
mock_get_links.return_value = [{
|
517
|
+
consts.STATE: consts.IFACE_DOWN
|
518
|
+
}]
|
519
|
+
|
520
|
+
controller = interface.InterfaceController()
|
521
|
+
controller.up(iface)
|
522
|
+
|
523
|
+
mock_link.assert_called_once_with(
|
524
|
+
controller.SET,
|
525
|
+
index=idx,
|
526
|
+
state=consts.IFACE_UP,
|
527
|
+
mtu=1450)
|
528
|
+
|
529
|
+
mock_addr.assert_has_calls([
|
530
|
+
mock.call(controller.ADD,
|
531
|
+
index=idx,
|
532
|
+
address='192.0.2.4',
|
533
|
+
prefixlen=24,
|
534
|
+
family=socket.AF_INET),
|
535
|
+
mock.call(controller.ADD,
|
536
|
+
index=idx,
|
537
|
+
address='198.51.100.4',
|
538
|
+
prefixlen=16,
|
539
|
+
family=socket.AF_INET),
|
540
|
+
mock.call(controller.ADD,
|
541
|
+
index=idx,
|
542
|
+
address='2001:db8::3',
|
543
|
+
prefixlen=64,
|
544
|
+
family=socket.AF_INET6)
|
545
|
+
])
|
546
|
+
|
547
|
+
mock_route.assert_has_calls([
|
548
|
+
mock.call(controller.ADD,
|
549
|
+
oif=idx,
|
550
|
+
dst='203.0.113.0/24',
|
551
|
+
gateway='192.0.2.1',
|
552
|
+
table=10,
|
553
|
+
onlink=True,
|
554
|
+
family=socket.AF_INET),
|
555
|
+
mock.call(controller.ADD,
|
556
|
+
oif=idx,
|
557
|
+
dst='198.51.100.0/24',
|
558
|
+
gateway='192.0.2.2',
|
559
|
+
prefsrc='192.0.2.4',
|
560
|
+
scope='link',
|
561
|
+
family=socket.AF_INET),
|
562
|
+
mock.call(controller.ADD,
|
563
|
+
oif=idx,
|
564
|
+
dst='2001:db8:2::1/128',
|
565
|
+
gateway='2001:db8::1',
|
566
|
+
family=socket.AF_INET6)])
|
567
|
+
|
568
|
+
mock_rule.assert_has_calls([
|
569
|
+
mock.call(controller.ADD,
|
570
|
+
src="203.0.113.1",
|
571
|
+
src_len=32,
|
572
|
+
table=20,
|
573
|
+
family=socket.AF_INET),
|
574
|
+
mock.call(controller.ADD,
|
575
|
+
src="2001:db8::1",
|
576
|
+
src_len=128,
|
577
|
+
table=40,
|
578
|
+
family=socket.AF_INET6)])
|
579
|
+
|
580
|
+
mock_check_output.assert_has_calls([
|
581
|
+
mock.call([consts.NFT_CMD, consts.NFT_ADD, 'table',
|
582
|
+
consts.NFT_FAMILY, consts.NFT_VIP_TABLE], stderr=-2),
|
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),
|
591
|
+
mock.call(["post-up", "fake-eth1"])
|
592
|
+
])
|
593
|
+
|
594
|
+
mock_nftable.assert_called_once_with('fake-eth1', [])
|
595
|
+
|
451
596
|
@mock.patch('pyroute2.IPRoute.rule')
|
452
597
|
@mock.patch('pyroute2.IPRoute.route')
|
453
598
|
@mock.patch('pyroute2.IPRoute.addr')
|
@@ -714,7 +859,9 @@ class TestInterface(base.TestCase):
|
|
714
859
|
table=254,
|
715
860
|
family=socket.AF_INET)])
|
716
861
|
|
717
|
-
mock_check_output.
|
862
|
+
mock_check_output.assert_has_calls([
|
863
|
+
mock.call(["post-up", "eth1"])
|
864
|
+
])
|
718
865
|
|
719
866
|
@mock.patch('pyroute2.IPRoute.rule')
|
720
867
|
@mock.patch('pyroute2.IPRoute.route')
|
@@ -1297,3 +1444,56 @@ class TestInterface(base.TestCase):
|
|
1297
1444
|
|
1298
1445
|
addr = controller._normalize_ip_network(None)
|
1299
1446
|
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()
|
@@ -620,7 +620,7 @@ class LvsQueryTestCase(base.TestCase):
|
|
620
620
|
def test_get_lvs_listener_pool_status_when_not_get_realserver_result(
|
621
621
|
self, mock_get_mapping, mock_os_stat):
|
622
622
|
# This will hit if the kernel lvs file (/proc/net/ip_vs)
|
623
|
-
# lose its content. So at this moment,
|
623
|
+
# lose its content. So at this moment, even though we configure the
|
624
624
|
# pool and member into udp keepalived config file, we have to set
|
625
625
|
# ths status of pool and its members to DOWN.
|
626
626
|
mock_os_stat.side_effect = (
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# Copyright 2024 Red Hat, Inc. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
4
|
+
# not use this file except in compliance with the License. You may obtain
|
5
|
+
# a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
12
|
+
# License for the specific language governing permissions and limitations
|
13
|
+
# under the License.
|
14
|
+
import os
|
15
|
+
import stat
|
16
|
+
import subprocess
|
17
|
+
from unittest import mock
|
18
|
+
|
19
|
+
from octavia_lib.common import constants as lib_consts
|
20
|
+
from webob import exc
|
21
|
+
|
22
|
+
from octavia.amphorae.backends.utils import nftable_utils
|
23
|
+
from octavia.common import constants as consts
|
24
|
+
from octavia.common import exceptions
|
25
|
+
import octavia.tests.unit.base as base
|
26
|
+
|
27
|
+
|
28
|
+
class TestNFTableUtils(base.TestCase):
|
29
|
+
@mock.patch('os.open')
|
30
|
+
@mock.patch('os.path.isfile')
|
31
|
+
def test_write_nftable_vip_rules_file_exists(self, mock_isfile, mock_open):
|
32
|
+
"""Test when a rules file exists and no new rules
|
33
|
+
|
34
|
+
When an existing rules file is present and we call
|
35
|
+
write_nftable_vip_rules_file with no rules, the method should not
|
36
|
+
overwrite the existing rules.
|
37
|
+
"""
|
38
|
+
mock_isfile.return_value = True
|
39
|
+
|
40
|
+
nftable_utils.write_nftable_vip_rules_file('fake-eth2', [])
|
41
|
+
|
42
|
+
mock_open.assert_not_called()
|
43
|
+
|
44
|
+
@mock.patch('os.open')
|
45
|
+
@mock.patch('os.path.isfile')
|
46
|
+
def test_write_nftable_vip_rules_file_rules(self, mock_isfile,
|
47
|
+
mock_open):
|
48
|
+
"""Test when a rules file exists and rules are passed in
|
49
|
+
|
50
|
+
This should create a simple rules file with the base chain and rules.
|
51
|
+
"""
|
52
|
+
mock_isfile.return_value = True
|
53
|
+
mock_open.return_value = 'fake-fd'
|
54
|
+
|
55
|
+
test_rule_1 = {consts.CIDR: None,
|
56
|
+
consts.PROTOCOL: lib_consts.PROTOCOL_TCP,
|
57
|
+
consts.PORT: 1234}
|
58
|
+
test_rule_2 = {consts.CIDR: '192.0.2.0/24',
|
59
|
+
consts.PROTOCOL: consts.VRRP,
|
60
|
+
consts.PORT: 4321}
|
61
|
+
|
62
|
+
mocked_open = mock.mock_open()
|
63
|
+
with mock.patch.object(os, 'fdopen', mocked_open):
|
64
|
+
nftable_utils.write_nftable_vip_rules_file(
|
65
|
+
'fake-eth2', [test_rule_1, test_rule_2])
|
66
|
+
|
67
|
+
mocked_open.assert_called_once_with('fake-fd', 'w')
|
68
|
+
mock_open.assert_called_once_with(
|
69
|
+
consts.NFT_VIP_RULES_FILE,
|
70
|
+
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
71
|
+
(stat.S_IRUSR | stat.S_IWUSR))
|
72
|
+
|
73
|
+
handle = mocked_open()
|
74
|
+
handle.write.assert_has_calls([
|
75
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} '
|
76
|
+
'{}\n'),
|
77
|
+
mock.call(f'delete table {consts.NFT_FAMILY} '
|
78
|
+
f'{consts.NFT_VIP_TABLE}\n'),
|
79
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} '
|
80
|
+
'{\n'),
|
81
|
+
mock.call(f' chain {consts.NFT_VIP_CHAIN} {{\n'),
|
82
|
+
mock.call(' type filter hook ingress device fake-eth2 '
|
83
|
+
f'priority {consts.NFT_SRIOV_PRIORITY}; policy drop;\n'),
|
84
|
+
mock.call(' icmp type destination-unreachable accept\n'),
|
85
|
+
mock.call(' icmpv6 type { nd-neighbor-solicit, '
|
86
|
+
'nd-router-advert, nd-neighbor-advert, packet-too-big, '
|
87
|
+
'destination-unreachable } accept\n'),
|
88
|
+
mock.call(' udp sport 67 udp dport 68 accept\n'),
|
89
|
+
mock.call(' udp sport 547 udp dport 546 accept\n'),
|
90
|
+
mock.call(' tcp dport 1234 accept\n'),
|
91
|
+
mock.call(' ip saddr 192.0.2.0/24 ip protocol 112 accept\n'),
|
92
|
+
mock.call(' }\n'),
|
93
|
+
mock.call('}\n')
|
94
|
+
])
|
95
|
+
|
96
|
+
@mock.patch('os.open')
|
97
|
+
@mock.patch('os.path.isfile')
|
98
|
+
def test_write_nftable_vip_rules_file_missing(self, mock_isfile,
|
99
|
+
mock_open):
|
100
|
+
"""Test when a rules file does not exist and no new rules
|
101
|
+
|
102
|
+
This should create a simple rules file with the base chain.
|
103
|
+
"""
|
104
|
+
mock_isfile.return_value = False
|
105
|
+
mock_open.return_value = 'fake-fd'
|
106
|
+
|
107
|
+
mocked_open = mock.mock_open()
|
108
|
+
with mock.patch.object(os, 'fdopen', mocked_open):
|
109
|
+
nftable_utils.write_nftable_vip_rules_file('fake-eth2', [])
|
110
|
+
|
111
|
+
mocked_open.assert_called_once_with('fake-fd', 'w')
|
112
|
+
mock_open.assert_called_once_with(
|
113
|
+
consts.NFT_VIP_RULES_FILE,
|
114
|
+
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
115
|
+
(stat.S_IRUSR | stat.S_IWUSR))
|
116
|
+
|
117
|
+
handle = mocked_open()
|
118
|
+
handle.write.assert_has_calls([
|
119
|
+
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} '
|
120
|
+
'{\n'),
|
121
|
+
mock.call(f' chain {consts.NFT_VIP_CHAIN} {{\n'),
|
122
|
+
mock.call(' type filter hook ingress device fake-eth2 '
|
123
|
+
f'priority {consts.NFT_SRIOV_PRIORITY}; policy drop;\n'),
|
124
|
+
mock.call(' icmp type destination-unreachable accept\n'),
|
125
|
+
mock.call(' icmpv6 type { nd-neighbor-solicit, '
|
126
|
+
'nd-router-advert, nd-neighbor-advert, packet-too-big, '
|
127
|
+
'destination-unreachable } accept\n'),
|
128
|
+
mock.call(' udp sport 67 udp dport 68 accept\n'),
|
129
|
+
mock.call(' udp sport 547 udp dport 546 accept\n'),
|
130
|
+
mock.call(' }\n'),
|
131
|
+
mock.call('}\n')
|
132
|
+
])
|
133
|
+
|
134
|
+
@mock.patch('octavia.common.utils.ip_version')
|
135
|
+
def test__build_rule_cmd(self, mock_ip_version):
|
136
|
+
|
137
|
+
mock_ip_version.side_effect = [4, 6, 99]
|
138
|
+
|
139
|
+
cmd = nftable_utils._build_rule_cmd({
|
140
|
+
consts.CIDR: '192.0.2.0/24',
|
141
|
+
consts.PROTOCOL: lib_consts.PROTOCOL_SCTP,
|
142
|
+
consts.PORT: 1234})
|
143
|
+
self.assertEqual('ip saddr 192.0.2.0/24 sctp dport 1234 accept', cmd)
|
144
|
+
|
145
|
+
cmd = nftable_utils._build_rule_cmd({
|
146
|
+
consts.CIDR: '2001:db8::/32',
|
147
|
+
consts.PROTOCOL: lib_consts.PROTOCOL_TCP,
|
148
|
+
consts.PORT: 1235})
|
149
|
+
self.assertEqual('ip6 saddr 2001:db8::/32 tcp dport 1235 accept', cmd)
|
150
|
+
|
151
|
+
self.assertRaises(exc.HTTPBadRequest, nftable_utils._build_rule_cmd,
|
152
|
+
{consts.CIDR: '192/32',
|
153
|
+
consts.PROTOCOL: lib_consts.PROTOCOL_TCP,
|
154
|
+
consts.PORT: 1237})
|
155
|
+
|
156
|
+
cmd = nftable_utils._build_rule_cmd({
|
157
|
+
consts.CIDR: None,
|
158
|
+
consts.PROTOCOL: lib_consts.PROTOCOL_UDP,
|
159
|
+
consts.PORT: 1236})
|
160
|
+
self.assertEqual('udp dport 1236 accept', cmd)
|
161
|
+
|
162
|
+
cmd = nftable_utils._build_rule_cmd({
|
163
|
+
consts.CIDR: None,
|
164
|
+
consts.PROTOCOL: consts.VRRP,
|
165
|
+
consts.PORT: 1237})
|
166
|
+
self.assertEqual('ip protocol 112 accept', cmd)
|
167
|
+
|
168
|
+
self.assertRaises(exc.HTTPBadRequest, nftable_utils._build_rule_cmd,
|
169
|
+
{consts.CIDR: None,
|
170
|
+
consts.PROTOCOL: 'bad-protocol',
|
171
|
+
consts.PORT: 1237})
|
172
|
+
|
173
|
+
@mock.patch('octavia.amphorae.backends.utils.network_namespace.'
|
174
|
+
'NetworkNamespace')
|
175
|
+
@mock.patch('subprocess.check_output')
|
176
|
+
def test_load_nftables_file(self, mock_check_output, mock_netns):
|
177
|
+
|
178
|
+
mock_netns.side_effect = [
|
179
|
+
mock.DEFAULT,
|
180
|
+
subprocess.CalledProcessError(cmd=consts.NFT_CMD, returncode=-1),
|
181
|
+
exceptions.AmphoraNetworkConfigException]
|
182
|
+
|
183
|
+
nftable_utils.load_nftables_file()
|
184
|
+
|
185
|
+
mock_netns.assert_called_once_with(consts.AMPHORA_NAMESPACE)
|
186
|
+
mock_check_output.assert_called_once_with([
|
187
|
+
consts.NFT_CMD, '-o', '-f', consts.NFT_VIP_RULES_FILE],
|
188
|
+
stderr=subprocess.STDOUT)
|
189
|
+
|
190
|
+
self.assertRaises(subprocess.CalledProcessError,
|
191
|
+
nftable_utils.load_nftables_file)
|
192
|
+
|
193
|
+
self.assertRaises(exceptions.AmphoraNetworkConfigException,
|
194
|
+
nftable_utils.load_nftables_file)
|
@@ -13,7 +13,7 @@
|
|
13
13
|
# under the License.
|
14
14
|
from unittest import mock
|
15
15
|
|
16
|
-
from octavia.amphorae.driver_exceptions
|
16
|
+
from octavia.amphorae.driver_exceptions import exceptions as driver_except
|
17
17
|
from octavia.amphorae.drivers.haproxy import exceptions as exc
|
18
18
|
from octavia.amphorae.drivers.haproxy import rest_api_driver
|
19
19
|
import octavia.tests.unit.base as base
|
@@ -76,9 +76,9 @@ class TestHAProxyAmphoraDriver(base.TestCase):
|
|
76
76
|
mock_api_version.reset_mock()
|
77
77
|
client_mock.reset_mock()
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
79
|
+
self.assertRaises(
|
80
|
+
exc.NotFound,
|
81
|
+
self.driver.get_interface_from_ip, amphora_mock, IP_ADDRESS)
|
82
82
|
mock_api_version.assert_called_once_with(amphora_mock, None)
|
83
83
|
client_mock.get_interface.assert_called_once_with(
|
84
84
|
amphora_mock, IP_ADDRESS, None, log_error=False)
|
@@ -87,6 +87,28 @@ class TestHAProxyAmphoraDriver(base.TestCase):
|
|
87
87
|
mock_amp = mock.MagicMock()
|
88
88
|
mock_amp.api_version = "0.5"
|
89
89
|
|
90
|
-
self.assertRaises(AmpVersionUnsupported,
|
90
|
+
self.assertRaises(driver_except.AmpVersionUnsupported,
|
91
91
|
self.driver._populate_amphora_api_version,
|
92
92
|
mock_amp)
|
93
|
+
|
94
|
+
@mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.'
|
95
|
+
'HaproxyAmphoraLoadBalancerDriver.'
|
96
|
+
'_populate_amphora_api_version')
|
97
|
+
def test_set_interface_rules(self, mock_api_version):
|
98
|
+
|
99
|
+
IP_ADDRESS = '203.0.113.44'
|
100
|
+
amphora_mock = mock.MagicMock()
|
101
|
+
amphora_mock.api_version = '0'
|
102
|
+
client_mock = mock.MagicMock()
|
103
|
+
client_mock.set_interface_rules.side_effect = [mock.DEFAULT,
|
104
|
+
exc.NotFound]
|
105
|
+
self.driver.clients['0'] = client_mock
|
106
|
+
|
107
|
+
self.driver.set_interface_rules(amphora_mock, IP_ADDRESS, 'fake_rules')
|
108
|
+
mock_api_version.assert_called_once_with(amphora_mock, None)
|
109
|
+
client_mock.set_interface_rules.assert_called_once_with(
|
110
|
+
amphora_mock, IP_ADDRESS, 'fake_rules', timeout_dict=None)
|
111
|
+
|
112
|
+
self.assertRaises(driver_except.AmpDriverNotImplementedError,
|
113
|
+
self.driver.set_interface_rules, amphora_mock,
|
114
|
+
IP_ADDRESS, 'fake_rules')
|
@@ -113,7 +113,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|
113
113
|
'vrrp_ip': self.amp.vrrp_ip,
|
114
114
|
'mtu': FAKE_MTU,
|
115
115
|
'host_routes': host_routes_data,
|
116
|
-
'additional_vips': []
|
116
|
+
'additional_vips': [],
|
117
|
+
'is_sriov': False}
|
117
118
|
|
118
119
|
self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1,
|
119
120
|
constants.REQ_READ_TIMEOUT: 2,
|
@@ -766,6 +767,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|
766
767
|
'host_routes': netinfo['host_routes']
|
767
768
|
}
|
768
769
|
]
|
770
|
+
netinfo['is_sriov'] = False
|
769
771
|
self.driver.clients[API_VERSION].plug_vip.assert_called_once_with(
|
770
772
|
self.amp, self.lb.vip.ip_address, netinfo)
|
771
773
|
|
@@ -815,7 +817,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|
815
817
|
vrrp_ip=self.amp.vrrp_ip,
|
816
818
|
host_routes=[],
|
817
819
|
additional_vips=[],
|
818
|
-
mtu=FAKE_MTU
|
820
|
+
mtu=FAKE_MTU,
|
821
|
+
is_sriov=False
|
819
822
|
)))
|
820
823
|
|
821
824
|
def test_post_network_plug_with_host_routes(self):
|
@@ -1545,3 +1548,13 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
|
1545
1548
|
self.assertRaises(exc.InternalServerError,
|
1546
1549
|
self.driver.update_agent_config, self.amp,
|
1547
1550
|
"some_file")
|
1551
|
+
|
1552
|
+
@requests_mock.mock()
|
1553
|
+
def test_set_interface_rules(self, m):
|
1554
|
+
ip_addr = '192.0.2.44'
|
1555
|
+
rules = ('[{"protocol":"TCP","cidr":"192.0.2.0/24","port":8080},'
|
1556
|
+
'{"protocol":"UDP","cidr":null,"port":80}]')
|
1557
|
+
m.put(f'{self.base_url_ver}/interface/{ip_addr}/rules')
|
1558
|
+
|
1559
|
+
self.driver.set_interface_rules(self.amp, ip_addr, rules)
|
1560
|
+
self.assertTrue(m.called)
|
@@ -107,6 +107,9 @@ class TestVRRPRestDriver(base.TestCase):
|
|
107
107
|
|
108
108
|
self.keepalived_mixin.start_vrrp_service(self.amphora_mock)
|
109
109
|
|
110
|
+
populate_mock = self.keepalived_mixin._populate_amphora_api_version
|
111
|
+
populate_mock.assert_called_once_with(self.amphora_mock,
|
112
|
+
timeout_dict=None)
|
110
113
|
self.clients[API_VERSION].start_vrrp.assert_called_once_with(
|
111
114
|
self.amphora_mock, timeout_dict=None)
|
112
115
|
|
@@ -121,6 +124,20 @@ class TestVRRPRestDriver(base.TestCase):
|
|
121
124
|
|
122
125
|
self.clients[API_VERSION].start_vrrp.assert_not_called()
|
123
126
|
|
127
|
+
# With timeout_dict
|
128
|
+
self.clients[API_VERSION].start_vrrp.reset_mock()
|
129
|
+
populate_mock.reset_mock()
|
130
|
+
|
131
|
+
timeout_dict = mock.Mock()
|
132
|
+
self.keepalived_mixin.start_vrrp_service(self.amphora_mock,
|
133
|
+
timeout_dict=timeout_dict)
|
134
|
+
|
135
|
+
populate_mock = self.keepalived_mixin._populate_amphora_api_version
|
136
|
+
populate_mock.assert_called_once_with(self.amphora_mock,
|
137
|
+
timeout_dict=timeout_dict)
|
138
|
+
self.clients[API_VERSION].start_vrrp.assert_called_once_with(
|
139
|
+
self.amphora_mock, timeout_dict=timeout_dict)
|
140
|
+
|
124
141
|
def test_reload_vrrp_service(self):
|
125
142
|
|
126
143
|
self.keepalived_mixin.reload_vrrp_service(self.lb_mock)
|
@@ -57,7 +57,8 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase):
|
|
57
57
|
constants.CONN_MAX_RETRIES: 3,
|
58
58
|
constants.CONN_RETRY_INTERVAL: 4}
|
59
59
|
|
60
|
-
|
60
|
+
@mock.patch('octavia.db.api.get_session')
|
61
|
+
def test_update_amphora_listeners(self, mock_session):
|
61
62
|
self.driver.update_amphora_listeners(self.load_balancer, self.amphora,
|
62
63
|
self.timeout_dict)
|
63
64
|
self.assertEqual((self.listener, self.amphora.id, self.timeout_dict,
|
@@ -17,8 +17,12 @@ from wsme import exc
|
|
17
17
|
from wsme.rest import json as wsme_json
|
18
18
|
from wsme import types as wsme_types
|
19
19
|
|
20
|
+
from octavia.api.common import types
|
21
|
+
from octavia.api.v2.types import health_monitor as health_monitor_type
|
22
|
+
from octavia.api.v2.types import member as member_type
|
20
23
|
from octavia.api.v2.types import pool as pool_type
|
21
24
|
from octavia.common import constants
|
25
|
+
from octavia.common import data_models
|
22
26
|
from octavia.tests.unit.api.common import base
|
23
27
|
|
24
28
|
|
@@ -224,3 +228,70 @@ class TestSessionPersistencePUT(base.BaseTypesTest, TestSessionPersistence):
|
|
224
228
|
body = {"cookie_name": "cookie\nmonster"}
|
225
229
|
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
|
226
230
|
body)
|
231
|
+
|
232
|
+
|
233
|
+
class TestPoolResponse(base.BaseTypesTest):
|
234
|
+
|
235
|
+
_type = pool_type.PoolResponse
|
236
|
+
|
237
|
+
def test_pool_response_with_health_monitor(self):
|
238
|
+
health_monitor_id = uuidutils.generate_uuid()
|
239
|
+
health_monitor_model = data_models.HealthMonitor(id=health_monitor_id)
|
240
|
+
pool_model = data_models.Pool(health_monitor=health_monitor_model)
|
241
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
242
|
+
self.assertEqual(pool.healthmonitor_id, health_monitor_id)
|
243
|
+
|
244
|
+
def test_pool_response_with_members(self):
|
245
|
+
member_id = uuidutils.generate_uuid()
|
246
|
+
members = [data_models.Member(id=member_id)]
|
247
|
+
pool_model = data_models.Pool(members=members)
|
248
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
249
|
+
self.assertIsInstance(pool.members[0], types.IdOnlyType)
|
250
|
+
self.assertEqual(pool.members[0].id, member_id)
|
251
|
+
|
252
|
+
def test_pool_response_with_load_balancer(self):
|
253
|
+
load_balancer_id = uuidutils.generate_uuid()
|
254
|
+
load_balancer = data_models.LoadBalancer(id=load_balancer_id)
|
255
|
+
pool_model = data_models.Pool(load_balancer=load_balancer)
|
256
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
257
|
+
self.assertIsInstance(pool.loadbalancers[0], types.IdOnlyType)
|
258
|
+
self.assertEqual(pool.loadbalancers[0].id, load_balancer_id)
|
259
|
+
|
260
|
+
def test_pool_response_with_session_persistence(self):
|
261
|
+
session_persistence = data_models.SessionPersistence(
|
262
|
+
cookie_name="test"
|
263
|
+
)
|
264
|
+
pool_model = data_models.Pool(session_persistence=session_persistence)
|
265
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
266
|
+
self.assertEqual(pool.session_persistence.cookie_name, "test")
|
267
|
+
|
268
|
+
def test_pool_response_without_children(self):
|
269
|
+
pool = self._type.from_data_model(data_model=data_models.Pool())
|
270
|
+
self.assertEqual(len(pool.loadbalancers), 0)
|
271
|
+
self.assertIsNone(pool.session_persistence)
|
272
|
+
self.assertEqual(len(pool.members), 0)
|
273
|
+
self.assertEqual(len(pool.listeners), 0)
|
274
|
+
self.assertEqual(pool.healthmonitor_id, wsme_types.Unset)
|
275
|
+
|
276
|
+
|
277
|
+
class TestPoolFullResponse(base.BaseTypesTest):
|
278
|
+
|
279
|
+
_type = pool_type.PoolFullResponse
|
280
|
+
|
281
|
+
def test_pool_full_response_with_health_monitor(self):
|
282
|
+
health_monitor_model = data_models.HealthMonitor()
|
283
|
+
pool_model = data_models.Pool(health_monitor=health_monitor_model)
|
284
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
285
|
+
self.assertIsInstance(
|
286
|
+
pool.healthmonitor, health_monitor_type.HealthMonitorFullResponse
|
287
|
+
)
|
288
|
+
|
289
|
+
def test_pool_full_response_with_members(self):
|
290
|
+
members = [data_models.Member()]
|
291
|
+
pool_model = data_models.Pool(members=members)
|
292
|
+
pool = self._type.from_data_model(data_model=pool_model)
|
293
|
+
self.assertIsInstance(pool.members[0], member_type.MemberFullResponse)
|
294
|
+
|
295
|
+
def test_pool_full_response_without_children(self):
|
296
|
+
pool = self._type.from_data_model(data_model=data_models.Pool())
|
297
|
+
self.assertIsNone(pool.healthmonitor)
|