octavia 13.0.0__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.
Files changed (135) hide show
  1. octavia/amphorae/backends/agent/api_server/lvs_listener_base.py +1 -1
  2. octavia/amphorae/backends/agent/api_server/osutils.py +5 -5
  3. octavia/amphorae/backends/agent/api_server/plug.py +3 -2
  4. octavia/amphorae/backends/agent/api_server/rules_schema.py +52 -0
  5. octavia/amphorae/backends/agent/api_server/server.py +28 -1
  6. octavia/amphorae/backends/utils/interface.py +45 -6
  7. octavia/amphorae/backends/utils/interface_file.py +9 -6
  8. octavia/amphorae/backends/utils/nftable_utils.py +125 -0
  9. octavia/amphorae/drivers/driver_base.py +27 -0
  10. octavia/amphorae/drivers/haproxy/rest_api_driver.py +42 -10
  11. octavia/amphorae/drivers/health/heartbeat_udp.py +2 -2
  12. octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py +2 -1
  13. octavia/amphorae/drivers/noop_driver/driver.py +25 -0
  14. octavia/api/app.py +3 -0
  15. octavia/api/common/pagination.py +2 -2
  16. octavia/api/drivers/amphora_driver/flavor_schema.py +6 -1
  17. octavia/api/root_controller.py +4 -1
  18. octavia/api/v2/controllers/health_monitor.py +0 -1
  19. octavia/api/v2/controllers/l7policy.py +0 -1
  20. octavia/api/v2/controllers/l7rule.py +0 -1
  21. octavia/api/v2/controllers/listener.py +0 -1
  22. octavia/api/v2/controllers/load_balancer.py +13 -7
  23. octavia/api/v2/controllers/member.py +6 -3
  24. octavia/api/v2/controllers/pool.py +6 -7
  25. octavia/api/v2/types/load_balancer.py +5 -1
  26. octavia/api/v2/types/pool.py +1 -1
  27. octavia/certificates/common/pkcs12.py +9 -9
  28. octavia/certificates/manager/barbican.py +24 -16
  29. octavia/certificates/manager/castellan_mgr.py +12 -7
  30. octavia/certificates/manager/local.py +4 -4
  31. octavia/certificates/manager/noop.py +106 -0
  32. octavia/cmd/driver_agent.py +1 -1
  33. octavia/cmd/health_checker.py +0 -4
  34. octavia/cmd/health_manager.py +1 -5
  35. octavia/cmd/house_keeping.py +1 -1
  36. octavia/cmd/interface.py +0 -4
  37. octavia/cmd/octavia_worker.py +0 -4
  38. octavia/cmd/prometheus_proxy.py +0 -5
  39. octavia/cmd/status.py +0 -6
  40. octavia/common/base_taskflow.py +1 -1
  41. octavia/common/clients.py +15 -3
  42. octavia/common/config.py +24 -6
  43. octavia/common/constants.py +34 -0
  44. octavia/common/data_models.py +3 -1
  45. octavia/common/exceptions.py +11 -0
  46. octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 +7 -5
  47. octavia/common/keystone.py +7 -7
  48. octavia/common/tls_utils/cert_parser.py +24 -10
  49. octavia/common/utils.py +6 -0
  50. octavia/common/validate.py +2 -2
  51. octavia/compute/drivers/nova_driver.py +23 -5
  52. octavia/controller/worker/task_utils.py +28 -6
  53. octavia/controller/worker/v2/controller_worker.py +49 -15
  54. octavia/controller/worker/v2/flows/amphora_flows.py +120 -21
  55. octavia/controller/worker/v2/flows/flow_utils.py +15 -13
  56. octavia/controller/worker/v2/flows/listener_flows.py +95 -5
  57. octavia/controller/worker/v2/flows/load_balancer_flows.py +74 -30
  58. octavia/controller/worker/v2/taskflow_jobboard_driver.py +17 -1
  59. octavia/controller/worker/v2/tasks/amphora_driver_tasks.py +145 -24
  60. octavia/controller/worker/v2/tasks/compute_tasks.py +1 -1
  61. octavia/controller/worker/v2/tasks/database_tasks.py +72 -41
  62. octavia/controller/worker/v2/tasks/lifecycle_tasks.py +97 -41
  63. octavia/controller/worker/v2/tasks/network_tasks.py +57 -60
  64. octavia/controller/worker/v2/tasks/shim_tasks.py +28 -0
  65. octavia/db/migration/alembic_migrations/versions/55874a4ceed6_add_l7policy_action_redirect_prefix.py +1 -1
  66. octavia/db/migration/alembic_migrations/versions/5a3ee5472c31_add_cert_expiration__infor_in_amphora_table.py +1 -1
  67. octavia/db/migration/alembic_migrations/versions/6742ca1b27c2_add_l7policy_redirect_http_code.py +1 -1
  68. octavia/db/migration/alembic_migrations/versions/db2a73e82626_add_vnic_type_for_vip.py +36 -0
  69. octavia/db/models.py +1 -0
  70. octavia/db/prepare.py +1 -1
  71. octavia/db/repositories.py +53 -34
  72. octavia/distributor/drivers/driver_base.py +1 -1
  73. octavia/network/base.py +3 -16
  74. octavia/network/data_models.py +4 -1
  75. octavia/network/drivers/neutron/allowed_address_pairs.py +27 -26
  76. octavia/network/drivers/noop_driver/driver.py +10 -23
  77. octavia/tests/common/sample_certs.py +115 -0
  78. octavia/tests/common/sample_haproxy_prometheus +1 -1
  79. octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +37 -0
  80. octavia/tests/functional/api/test_healthcheck.py +2 -2
  81. octavia/tests/functional/api/v2/base.py +1 -1
  82. octavia/tests/functional/api/v2/test_listener.py +45 -0
  83. octavia/tests/functional/api/v2/test_load_balancer.py +17 -0
  84. octavia/tests/functional/db/base.py +9 -0
  85. octavia/tests/functional/db/test_models.py +2 -1
  86. octavia/tests/functional/db/test_repositories.py +55 -99
  87. octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py +4 -2
  88. octavia/tests/unit/amphorae/backends/utils/test_interface.py +201 -1
  89. octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py +1 -1
  90. octavia/tests/unit/amphorae/backends/utils/test_nftable_utils.py +194 -0
  91. octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +27 -5
  92. octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py +15 -2
  93. octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py +17 -0
  94. octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py +2 -1
  95. octavia/tests/unit/api/v2/types/test_pool.py +71 -0
  96. octavia/tests/unit/certificates/manager/test_barbican.py +3 -3
  97. octavia/tests/unit/certificates/manager/test_noop.py +53 -0
  98. octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py +16 -17
  99. octavia/tests/unit/common/sample_configs/sample_configs_combined.py +5 -3
  100. octavia/tests/unit/common/test_config.py +35 -0
  101. octavia/tests/unit/common/test_keystone.py +32 -0
  102. octavia/tests/unit/common/test_utils.py +39 -0
  103. octavia/tests/unit/compute/drivers/test_nova_driver.py +22 -0
  104. octavia/tests/unit/controller/worker/test_task_utils.py +58 -2
  105. octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py +28 -5
  106. octavia/tests/unit/controller/worker/v2/flows/test_listener_flows.py +64 -16
  107. octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +49 -9
  108. octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py +265 -17
  109. octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks.py +101 -1
  110. octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py +19 -19
  111. octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +105 -42
  112. octavia/tests/unit/controller/worker/v2/tasks/test_shim_tasks.py +33 -0
  113. octavia/tests/unit/controller/worker/v2/test_controller_worker.py +85 -42
  114. octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +48 -51
  115. octavia/tests/unit/network/drivers/neutron/test_utils.py +2 -0
  116. octavia/tests/unit/network/drivers/noop_driver/test_driver.py +0 -7
  117. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/README.rst +6 -1
  118. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/diskimage-create.sh +10 -4
  119. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/requirements.txt +0 -2
  120. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/tox.ini +30 -13
  121. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/AUTHORS +5 -0
  122. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/METADATA +6 -6
  123. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/RECORD +134 -126
  124. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/entry_points.txt +1 -1
  125. octavia-14.0.0.dist-info/pbr.json +1 -0
  126. octavia-13.0.0.dist-info/pbr.json +0 -1
  127. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/LICENSE +0 -0
  128. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/README.rst +0 -0
  129. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/image-tests.sh +0 -0
  130. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/test-requirements.txt +0 -0
  131. {octavia-13.0.0.data → octavia-14.0.0.data}/data/share/octavia/diskimage-create/version.txt +0 -0
  132. {octavia-13.0.0.data → octavia-14.0.0.data}/scripts/octavia-wsgi +0 -0
  133. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/LICENSE +0 -0
  134. {octavia-13.0.0.dist-info → octavia-14.0.0.dist-info}/WHEEL +0 -0
  135. {octavia-13.0.0.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.assert_not_called()
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, eventhough we configure the
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.exceptions import AmpVersionUnsupported
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
- result = self.driver.get_interface_from_ip(amphora_mock, IP_ADDRESS)
80
-
81
- self.assertIsNone(result)
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
- def test_update_amphora_listeners(self):
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)