From 06ce4777c3bad2868e906101bcd2cd7c753b4859 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Thu, 27 Jun 2019 16:00:22 -0700 Subject: [PATCH] Fix multi-listener load balancers Load balancers with multiple listeners, running on an amphora image with HAProxy 1.8 or newer can experience excessive memory usage that may lead to an ERROR provisioning_status. This patch resolves this issue by consolidating the listeners into a single haproxy process inside the amphora. Story: 2005412 Task: 34744 Co-Authored-By: Adam Harwell Change-Id: Idaccbcfa0126f1e26fbb3ad770c65c9266cfad5b --- .../contributor/api/haproxy-amphora-api.rst | 350 +---- .../backends/agent/api_server/__init__.py | 2 +- .../backends/agent/api_server/amphora_info.py | 12 +- .../backends/agent/api_server/keepalived.py | 4 +- .../agent/api_server/keepalivedlvs.py | 44 +- .../{listener.py => loadbalancer.py} | 247 ++-- .../backends/agent/api_server/server.py | 74 +- .../agent/api_server/udp_listener_base.py | 11 - .../backends/agent/api_server/util.py | 106 +- .../backends/health_daemon/health_daemon.py | 77 +- .../amphorae/backends/utils/haproxy_query.py | 4 +- octavia/amphorae/drivers/driver_base.py | 72 +- .../drivers/haproxy/rest_api_driver.py | 574 ++++++-- .../drivers/keepalived/vrrp_rest_driver.py | 15 +- .../amphorae/drivers/noop_driver/driver.py | 70 +- octavia/cmd/health_manager.py | 3 + octavia/cmd/octavia_worker.py | 3 + .../haproxy/combined_listeners/__init__.py | 0 .../haproxy/combined_listeners/jinja_cfg.py | 499 +++++++ .../combined_listeners/templates/base.j2 | 52 + .../templates/haproxy.cfg.j2 | 40 + .../combined_listeners/templates/macros.j2 | 370 +++++ .../jinja/haproxy/split_listeners/__init__.py | 0 .../{ => split_listeners}/jinja_cfg.py | 0 .../{ => split_listeners}/templates/base.j2 | 0 .../templates/haproxy.cfg.j2 | 0 .../{ => split_listeners}/templates/macros.j2 | 0 .../healthmanager/health_drivers/update_db.py | 98 +- .../controller/worker/v1/controller_worker.py | 3 +- .../worker/v1/flows/amphora_flows.py | 5 +- .../worker/v1/flows/health_monitor_flows.py | 6 +- .../worker/v1/flows/l7policy_flows.py | 6 +- .../worker/v1/flows/l7rule_flows.py | 6 +- .../worker/v1/flows/listener_flows.py | 8 +- .../worker/v1/flows/load_balancer_flows.py | 2 +- .../worker/v1/flows/member_flows.py | 8 +- .../controller/worker/v1/flows/pool_flows.py | 6 +- .../worker/v1/tasks/amphora_driver_tasks.py | 59 +- .../controller/worker/v2/controller_worker.py | 3 +- .../worker/v2/flows/amphora_flows.py | 5 +- .../worker/v2/flows/health_monitor_flows.py | 6 +- .../worker/v2/flows/l7policy_flows.py | 6 +- .../worker/v2/flows/l7rule_flows.py | 6 +- .../worker/v2/flows/listener_flows.py | 8 +- .../worker/v2/flows/load_balancer_flows.py | 2 +- .../worker/v2/flows/member_flows.py | 8 +- .../controller/worker/v2/flows/pool_flows.py | 6 +- .../worker/v2/tasks/amphora_driver_tasks.py | 59 +- octavia/db/repositories.py | 4 +- .../agent/api_server/test_keepalivedlvs.py | 62 - .../backend/agent/api_server/test_server.py | 300 ++-- .../tests/functional/db/test_repositories.py | 3 +- .../agent/api_server/test_amphora_info.py | 43 +- .../api_server/test_haproxy_compatibility.py | 14 +- .../agent/api_server/test_keepalivedlvs.py | 8 - ...{test_listener.py => test_loadbalancer.py} | 230 ++- .../backends/agent/api_server/test_util.py | 104 +- .../health_daemon/test_health_daemon.py | 50 +- .../backends/utils/test_haproxy_query.py | 49 +- ..._driver.py => test_rest_api_driver_0_5.py} | 647 ++++---- .../haproxy/test_rest_api_driver_1_0.py | 1299 +++++++++++++++++ .../keepalived/test_vrrp_rest_driver.py | 22 +- .../test_noop_amphoraloadbalancer_driver.py | 29 +- .../certificates/manager/test_barbican.py | 3 +- .../haproxy/combined_listeners/__init__.py | 0 .../combined_listeners/test_jinja_cfg.py | 1205 +++++++++++++++ .../jinja/haproxy/split_listeners/__init__.py | 0 .../{ => split_listeners}/test_jinja_cfg.py | 298 ++-- .../common/jinja/lvs/test_lvs_jinja_cfg.py | 55 +- .../sample_configs/sample_configs_combined.py | 1076 ++++++++++++++ ...ple_configs.py => sample_configs_split.py} | 58 +- .../unit/common/tls_utils/test_cert_parser.py | 12 +- .../health_drivers/test_update_db.py | 127 +- .../v1/flows/test_load_balancer_flows.py | 3 +- .../v1/tasks/test_amphora_driver_tasks.py | 111 +- .../worker/v1/test_controller_worker.py | 3 +- .../v2/flows/test_load_balancer_flows.py | 3 +- .../v2/tasks/test_amphora_driver_tasks.py | 111 +- .../worker/v2/test_controller_worker.py | 3 +- ...proxy-single-process-b17a3af3a97accea.yaml | 11 + 80 files changed, 6752 insertions(+), 2136 deletions(-) rename octavia/amphorae/backends/agent/api_server/{listener.py => loadbalancer.py} (67%) create mode 100644 octavia/common/jinja/haproxy/combined_listeners/__init__.py create mode 100644 octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py create mode 100644 octavia/common/jinja/haproxy/combined_listeners/templates/base.j2 create mode 100644 octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 create mode 100644 octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 create mode 100644 octavia/common/jinja/haproxy/split_listeners/__init__.py rename octavia/common/jinja/haproxy/{ => split_listeners}/jinja_cfg.py (100%) rename octavia/common/jinja/haproxy/{ => split_listeners}/templates/base.j2 (100%) rename octavia/common/jinja/haproxy/{ => split_listeners}/templates/haproxy.cfg.j2 (100%) rename octavia/common/jinja/haproxy/{ => split_listeners}/templates/macros.j2 (100%) rename octavia/tests/unit/amphorae/backends/agent/api_server/{test_listener.py => test_loadbalancer.py} (56%) rename octavia/tests/unit/amphorae/drivers/haproxy/{test_rest_api_driver.py => test_rest_api_driver_0_5.py} (69%) create mode 100644 octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py create mode 100644 octavia/tests/unit/common/jinja/haproxy/combined_listeners/__init__.py create mode 100644 octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py create mode 100644 octavia/tests/unit/common/jinja/haproxy/split_listeners/__init__.py rename octavia/tests/unit/common/jinja/haproxy/{ => split_listeners}/test_jinja_cfg.py (83%) create mode 100644 octavia/tests/unit/common/sample_configs/sample_configs_combined.py rename octavia/tests/unit/common/sample_configs/{sample_configs.py => sample_configs_split.py} (95%) create mode 100644 releasenotes/notes/haproxy-single-process-b17a3af3a97accea.yaml diff --git a/doc/source/contributor/api/haproxy-amphora-api.rst b/doc/source/contributor/api/haproxy-amphora-api.rst index 77d630c7b7..46bbf13c2c 100644 --- a/doc/source/contributor/api/haproxy-amphora-api.rst +++ b/doc/source/contributor/api/haproxy-amphora-api.rst @@ -29,9 +29,7 @@ communication is limited to fail-over protocols.) Versioning ---------- All Octavia APIs (including internal APIs like this one) are versioned. For the -purposes of this document, the initial version of this API shall be v0.5. (So, -any reference to a *:version* variable should be replaced with the literal -string 'v0.5'.) +purposes of this document, the initial version of this API shall be 1.0. Response codes -------------- @@ -74,136 +72,6 @@ a secure way (ex. memory filesystem). API === -Get amphora topology --------------------- -* **URL:** /*:version*/topology -* **Method:** GET -* **URL params:** none -* **Data params:** none -* **Success Response:** - - * Code: 200 - - * Content: JSON formatted listing of this amphora's configured topology. - -* **Error Response:** - - * none - -JSON Response attributes: - -* *hostname* - hostname of amphora -* *uuid* - uuid of amphora -* *topology* - One of: SINGLE, ACTIVE-STANDBY, ACTIVE-ACTIVE -* *role* - One of ACTIVE, STANDBY (only applicable to ACTIVE-STANDBY) -* *ha_ip* - only applicable to ACTIVE-STANDBY topology: Highly-available - routing IP address for the ACTIVE-STANDBY pair. - -**Examples** - -* Success code 200: - -:: - - JSON response: - { - 'hostname': 'octavia-haproxy-img-00328', - 'uuid': '6e2bc8a0-2548-4fb7-a5f0-fb1ef4a696ce', - 'topology': 'SINGLE', - 'role': 'ACTIVE', - 'ha_ip': '', - } - -Set amphora topology --------------------- -* **URL:** /*:version*/topology -* **Method:** POST -* **URL params:** none -* **Data params:** - - * *topology*: One of: SINGLE, ACTIVE-STANDBY, ACTIVE-ACTIVE - * *role*: One of: ACTIVE, STANDBY (only applicable to ACTIVE-STANDBY) - * *ha_ip*: (only applicable to ACTIVE-STANDBY) Highly-available IP for the - HA pair - * *secret*: (only applicable to ACTIVE-STANDBY topology) Shared secret used - for authentication with other HA pair member - -* **Success Response:** - - * Code: 200 - - * Content: OK - - * Code: 202 - - * Content: OK - -* **Error Response:** - - * Code: 400 - - * Content: Invalid request. - * *(Response will also include information on which parameters did not* - *pass either a syntax check or other topology logic test)* - - * Code: 503 - - * Content: Topology transition in progress - -* **Response:** - -| OK - -**Notes:** In an ACTIVE-STANDBY configuration, the 'role' parameter might -change spontaneously due to a failure of one node. In other topologies, the -role is not used. - -Also note that some topology changes can take several minutes to enact, yet -we want all API commands to return in a matter of seconds. In this case, a -topology change is initiated, and the amphora status changes from "OK" to -"TOPOLOGY-CHANGE". The controller should not try to change any resources during -this transition. (Any attempts will be met with an error.) Once the -topology change is complete, amphora status should return to "OK". (When the -UDP communication from amphorae to controller is defined, a 'transition -complete' message is probably one good candidate for this type of UDP -communication.) - -**Examples** - -* Success code 200: - -:: - - JSON POST parameters: - { - 'topology': 'ACTIVE-STANDBY', - 'role': 'ACTIVE', - 'ha_ip': ' 203.0.113.2', - 'secret': 'b20e06cf1abcf29c708d3b437f4a29892a0921d0', - } - - Response: - OK - -* Error code 400: - -:: - - Response: - { - 'message': 'Invalid request', - 'details': 'Unknown topology: BAD_TEST_DATA', - } - -* Error code 503: - -:: - - Response: - { - 'message': 'Topology transition in progress', - } - Get amphora info ---------------- * **URL:** /info @@ -249,7 +117,7 @@ version string prepended to it. Get amphora details ------------------- -* **URL:** /*:version*/details +* **URL:** /1.0/details * **Method:** GET * **URL params:** none * **Data params:** none @@ -372,7 +240,7 @@ health of the amphora, currently-configured topology and role, etc. Get interface ------------- -* **URL:** /*:version*/interface/*:ip* +* **URL:** /1.0/interface/*:ip* * **Method:** GET * **URL params:** @@ -408,7 +276,7 @@ Get interface :: GET URL: - https://octavia-haproxy-img-00328.local/v0.5/interface/10.0.0.1 + https://octavia-haproxy-img-00328.local/1.0/interface/10.0.0.1 JSON Response: { @@ -422,7 +290,7 @@ Get interface :: GET URL: - https://octavia-haproxy-img-00328.local/v0.5/interface/10.5.0.1 + https://octavia-haproxy-img-00328.local/1.0/interface/10.5.0.1 JSON Response: { @@ -435,7 +303,7 @@ Get interface :: GET URL: - https://octavia-haproxy-img-00328.local/v0.5/interface/10.6.0.1.1 + https://octavia-haproxy-img-00328.local/1.0/interface/10.6.0.1.1 JSON Response: { @@ -446,7 +314,7 @@ Get interface Get all listeners' statuses --------------------------- -* **URL:** /*:version*/listeners +* **URL:** /1.0/listeners * **Method:** GET * **URL params:** none * **Data params:** none @@ -492,91 +360,14 @@ a valid haproxy configuration). 'type': 'TERMINATED_HTTPS', }] -Get a listener's status ------------------------ +Start or Stop a load balancer +----------------------------- -* **URL:** /*:version*/listeners/*:listener* -* **Method:** GET -* **URL params:** - - * *:listener* = Listener UUID - -* **Data params:** none -* **Success Response:** - - * Code: 200 - - * Content: JSON-formatted listener status - -* **Error Response:** - - * Code: 404 - - * Content: Not Found - -JSON Response attributes: - -* *status* - One of the operational status: ACTIVE, STOPPED, ERROR - - future versions might support provisioning status: - PENDING_CREATE, PENDING_UPDATE, PENDING_DELETE, DELETED -* *uuid* - Listener UUID -* *type* - One of: TCP, HTTP, TERMINATED_HTTPS -* *pools* - Map of pool UUIDs and their overall UP / DOWN / DEGRADED status -* *members* - Map of member UUIDs and their overall UP / DOWN status - - -**Notes:** Note that this returns a status if: the pid file exists, -the stats socket exists, or an haproxy configuration is present (not -just if there is a valid haproxy configuration). - -**Examples** - -* Success code 200: - -:: - - JSON Response: - { - 'status': 'ACTIVE', - 'uuid': 'e2dfddc0-5b9e-11e4-8ed6-0800200c9a66', - 'type': 'HTTP', - 'pools':[ - { - 'uuid': '399bbf4b-5f6c-4370-a61e-ed2ff2fc9387', - 'status': 'UP', - 'members':[ - {'73f6d278-ae1c-4248-ad02-0bfd50d69aab': 'UP'}, - {'2edca57c-5890-4bcb-ae67-4ef75776cc67': 'DOWN'}, - ], - }, - { - 'uuid': '2250eb21-16ca-44bd-9b12-0b4eb3d18140', - 'status': 'DOWN', - 'members':[ - {'130dff11-4aab-4ba8-a39b-8d77caa7a1ad': 'DOWN'}, - ], - }, - ], - } - -* Error code 404: - -:: - - JSON Response: - { - 'message': 'Listener Not Found', - 'details': 'No listener with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', - } - -Start or Stop a listener ------------------------- - -* **URL:** /*:version*/listeners/*:listener*/*:action* +* **URL:** /1.0/loadbalancer/*:object_id*/*:action* * **Method:** PUT * **URL params:** - * *:listener* = Listener UUID + * *:object_id* = Object UUID * *:action* = One of: start, stop, reload * **Data params:** none @@ -612,7 +403,7 @@ Start or Stop a listener | OK | Configuration file is valid -| haproxy daemon for 7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c started (pid 32428) +| haproxy daemon for 85e2111b-29c4-44be-94f3-e72045805801 started (pid 32428) **Examples:** @@ -621,12 +412,12 @@ Start or Stop a listener :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/start + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/start JSON Response: { 'message': 'OK', - 'details': 'Configuration file is valid\nhaproxy daemon for 04bff5c3-5862-4a13-b9e3-9b440d0ed50a started', + 'details': 'Configuration file is valid\nhaproxy daemon for 85e2111b-29c4-44be-94f3-e72045805801 started', } * Error code 400: @@ -634,7 +425,7 @@ Start or Stop a listener :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/BAD_TEST_DATA + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/BAD_TEST_DATA JSON Response: { @@ -647,12 +438,12 @@ Start or Stop a listener :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop JSON Response: { 'message': 'Listener Not Found', - 'details': 'No listener with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', + 'details': 'No loadbalancer with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', } * Error code 500: @@ -660,7 +451,7 @@ Start or Stop a listener :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/stop Response: { @@ -680,7 +471,7 @@ Start or Stop a listener Delete a listener ----------------- -* **URL:** /*:version*/listeners/*:listener* +* **URL:** /1.0/listeners/*:listener* * **Method:** DELETE * **URL params:** @@ -724,7 +515,7 @@ Delete a listener :: DELETE URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a + https://octavia-haproxy-img-00328.local/1.0/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a JSON Response: { @@ -736,7 +527,7 @@ Delete a listener :: DELETE URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a + https://octavia-haproxy-img-00328.local/1.0/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a JSON Response: { @@ -756,11 +547,11 @@ Delete a listener Upload SSL certificate PEM file ------------------------------- -* **URL:** /*:version*/listeners/*:listener*/certificates/*:filename.pem* +* **URL:** /1.0/loadbalancer/*:loadbalancer_id*/certificates/*:filename.pem* * **Method:** PUT * **URL params:** - * *:listener* = Listener UUID + * *:loadbalancer_id* = Load balancer UUID * *:filename* = PEM filename (see notes below for naming convention) * **Data params:** Certificate data. (PEM file should be a concatenation of @@ -812,7 +603,7 @@ explicitly restarted :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem (Put data should contain the certificate information, concatenated as described above) @@ -826,7 +617,7 @@ explicitly restarted :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem (If PUT data does not contain a certificate) JSON Response: @@ -839,7 +630,7 @@ explicitly restarted :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem (If PUT data does not contain an RSA key) JSON Response: @@ -852,7 +643,7 @@ explicitly restarted :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem (If the first certificate and the RSA key do not have the same modulus.) JSON Response: @@ -865,15 +656,14 @@ explicitly restarted :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem JSON Response: { 'message': 'Listener Not Found', - 'details': 'No listener with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', + 'details': 'No loadbalancer with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', } - * Error code 503: :: @@ -883,15 +673,14 @@ explicitly restarted 'message': 'Topology transition in progress', } - Get SSL certificate md5sum -------------------------- -* **URL:** /*:version*/listeners/*:listener*/certificates/*:filename.pem* +* **URL:** /1.0/loadbalancer/*:loadbalancer_id*/certificates/*:filename.pem* * **Method:** GET * **URL params:** - * *:listener* = Listener UUID + * *:loadbalancer_id* = Load balancer UUID * *:filename* = PEM filename (see notes below for naming convention) * **Data params:** none @@ -937,7 +726,7 @@ disclosing it over the wire from the amphora is a security risk. JSON Response: { 'message': 'Listener Not Found', - 'details': 'No listener with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', + 'details': 'No loadbalancer with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', } * Error code 404: @@ -953,11 +742,11 @@ disclosing it over the wire from the amphora is a security risk. Delete SSL certificate PEM file ------------------------------- -* **URL:** /*:version*/listeners/*:listener*/certificates/*:filename.pem* +* **URL:** /1.0/loadbalancer/*:loadbalancer_id*/certificates/*:filename.pem* * **Method:** DELETE * **URL params:** - * *:listener* = Listener UUID + * *:loadbalancer_id* = Load balancer UUID * *:filename* = PEM filename (see notes below for naming convention) * **Data params:** none @@ -988,7 +777,7 @@ Delete SSL certificate PEM file :: DELETE URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem JSON Response: { @@ -1000,7 +789,7 @@ Delete SSL certificate PEM file :: DELETE URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/certificates/www.example.com.pem JSON Response: { @@ -1017,14 +806,14 @@ Delete SSL certificate PEM file 'message': 'Topology transition in progress', } -Upload listener haproxy configuration -------------------------------------- +Upload load balancer haproxy configuration +------------------------------------------ -* **URL:** /*:version*/listeners/*:amphora_id*/*:listener*/haproxy +* **URL:** /1.0/loadbalancer/*:amphora_id*/*:loadbalancer_id*/haproxy * **Method:** PUT * **URL params:** - * *:listener* = Listener UUID + * *:loadbalancer_id* = Load Balancer UUID * *:amphora_id* = Amphora UUID * **Data params:** haproxy configuration file for the listener @@ -1071,7 +860,7 @@ out of the haproxy daemon status interface for tracking health and stats). :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/d459b1c8-54b0-4030-9bec-4f449e73b1ef/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/haproxy + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/d459b1c8-54b0-4030-9bec-4f449e73b1ef/85e2111b-29c4-44be-94f3-e72045805801/haproxy (Upload PUT data should be a raw haproxy.conf file.) JSON Response: @@ -1098,14 +887,14 @@ out of the haproxy daemon status interface for tracking health and stats). 'message': 'Topology transition in progress', } -Get listener haproxy configuration ----------------------------------- +Get loadbalancer haproxy configuration +-------------------------------------- -* **URL:** /*:version*/listeners/*:listener*/haproxy +* **URL:** /1.0/loadbalancer/*:loadbalancer_id*/haproxy * **Method:** GET * **URL params:** - * *:listener* = Listener UUID + * *:loadbalancer_id* = Load balancer UUID * **Data params:** none * **Success Response:** @@ -1122,7 +911,7 @@ Get listener haproxy configuration * **Response:** -| # Config file for 7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c +| # Config file for 85e2111b-29c4-44be-94f3-e72045805801 | (cut for brevity) * **Implied actions:** none @@ -1134,11 +923,11 @@ Get listener haproxy configuration :: GET URL: - https://octavia-haproxy-img-00328.local/v0.5/listeners/7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c/haproxy + https://octavia-haproxy-img-00328.local/1.0/loadbalancer/85e2111b-29c4-44be-94f3-e72045805801/haproxy Response is the raw haproxy.cfg: - # Config file for 7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c + # Config file for 85e2111b-29c4-44be-94f3-e72045805801 (cut for brevity) * Error code 404: @@ -1147,15 +936,14 @@ Get listener haproxy configuration JSON Response: { - 'message': 'Listener Not Found', - 'details': 'No listener with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', + 'message': 'Loadbalancer Not Found', + 'details': 'No loadbalancer with UUID: 04bff5c3-5862-4a13-b9e3-9b440d0ed50a', } - Plug VIP -------- -* **URL:** /*:version*/plug/vip/*:ip* +* **URL:** /1.0/plug/vip/*:ip* * **Method:** Post * **URL params:** @@ -1210,7 +998,7 @@ Plug VIP :: POST URL: - https://octavia-haproxy-img-00328.local/v0.5/plug/vip/203.0.113.2 + https://octavia-haproxy-img-00328.local/1.0/plug/vip/203.0.113.2 JSON POST parameters: { @@ -1225,10 +1013,6 @@ Plug VIP 'details': 'VIP 203.0.113.2 plugged on interface eth1' } - - - - * Error code 400: :: @@ -1251,7 +1035,7 @@ Plug VIP Plug Network ------------ -* **URL:** /*:version*/plug/network/ +* **URL:** /1.0/plug/network/ * **Method:** POST * **URL params:** none @@ -1292,7 +1076,7 @@ Plug Network :: POST URL: - https://octavia-haproxy-img-00328.local/v0.5/plug/network/ + https://octavia-haproxy-img-00328.local/1.0/plug/network/ JSON POST parameters: { @@ -1319,7 +1103,7 @@ Plug Network Upload SSL server certificate PEM file for Controller Communication ------------------------------------------------------------------- -* **URL:** /*:version*/certificate +* **URL:** /1.0/certificate * **Method:** PUT * **Data params:** Certificate data. (PEM file should be a concatenation of @@ -1362,7 +1146,7 @@ not be available for some time. :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/certificate + https://octavia-haproxy-img-00328.local/1.0/certificate (Put data should contain the certificate information, concatenated as described above) @@ -1376,7 +1160,7 @@ not be available for some time. :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/certificates + https://octavia-haproxy-img-00328.local/1.0/certificates (If PUT data does not contain a certificate) JSON Response: @@ -1389,7 +1173,7 @@ not be available for some time. :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/certificate + https://octavia-haproxy-img-00328.local/1.0/certificate (If PUT data does not contain an RSA key) JSON Response: @@ -1402,7 +1186,7 @@ not be available for some time. :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/certificate + https://octavia-haproxy-img-00328.local/1.0/certificate (If the first certificate and the RSA key do not have the same modulus.) JSON Response: @@ -1414,7 +1198,7 @@ not be available for some time. Upload keepalived configuration ------------------------------- -* **URL:** /*:version*/vrrp/upload +* **URL:** /1.0/vrrp/upload * **Method:** PUT * **URL params:** none * **Data params:** none @@ -1441,7 +1225,7 @@ OK :: PUT URI: - https://octavia-haproxy-img-00328.local/v0.5/vrrp/upload + https://octavia-haproxy-img-00328.local/1.0/vrrp/upload JSON Response: { @@ -1452,7 +1236,7 @@ OK Start, Stop, or Reload keepalived --------------------------------- -* **URL:** /*:version*/vrrp/*:action* +* **URL:** /1.0/vrrp/*:action* * **Method:** PUT * **URL params:** @@ -1489,7 +1273,7 @@ Start, Stop, or Reload keepalived :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/vrrp/start + https://octavia-haproxy-img-00328.local/1.0/vrrp/start JSON Response: { @@ -1502,7 +1286,7 @@ Start, Stop, or Reload keepalived :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/vrrp/BAD_TEST_DATA + https://octavia-haproxy-img-00328.local/1.0/vrrp/BAD_TEST_DATA JSON Response: { @@ -1515,7 +1299,7 @@ Start, Stop, or Reload keepalived :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/vrrp/stop + https://octavia-haproxy-img-00328.local/1.0/vrrp/stop JSON Response: { @@ -1526,7 +1310,7 @@ Start, Stop, or Reload keepalived Update the amphora agent configuration -------------------------------------- -* **URL:** /*:version*/config +* **URL:** /1.0/config * **Method:** PUT * **Data params:** A amphora-agent configuration file @@ -1561,7 +1345,7 @@ will be updated. :: PUT URL: - https://octavia-haproxy-img-00328.local/v0.5/config + https://octavia-haproxy-img-00328.local/1.0/config (Upload PUT data should be a raw amphora-agent.conf file.) JSON Response: diff --git a/octavia/amphorae/backends/agent/api_server/__init__.py b/octavia/amphorae/backends/agent/api_server/__init__.py index f685c838fc..5de10d17a2 100644 --- a/octavia/amphorae/backends/agent/api_server/__init__.py +++ b/octavia/amphorae/backends/agent/api_server/__init__.py @@ -12,4 +12,4 @@ # License for the specific language governing permissions and limitations # under the License. -VERSION = '0.5' +VERSION = '1.0' diff --git a/octavia/amphorae/backends/agent/api_server/amphora_info.py b/octavia/amphorae/backends/agent/api_server/amphora_info.py index a20fa07218..b27f16095c 100644 --- a/octavia/amphorae/backends/agent/api_server/amphora_info.py +++ b/octavia/amphorae/backends/agent/api_server/amphora_info.py @@ -46,7 +46,7 @@ class AmphoraInfo(object): return webob.Response(json=body) def compile_amphora_details(self, extend_udp_driver=None): - haproxy_listener_list = util.get_listeners() + haproxy_listener_list = sorted(util.get_listeners()) extend_body = {} udp_listener_list = [] if extend_udp_driver: @@ -87,8 +87,8 @@ class AmphoraInfo(object): 'load': self._load(), 'topology': consts.TOPOLOGY_SINGLE, 'topology_status': consts.TOPOLOGY_STATUS_OK, - 'listeners': list( - set(haproxy_listener_list + udp_listener_list)) + 'listeners': sorted(list( + set(haproxy_listener_list + udp_listener_list))) if udp_listener_list else haproxy_listener_list, 'packages': {}} if extend_body: @@ -101,10 +101,10 @@ class AmphoraInfo(object): version = subprocess.check_output(cmd.split()) return version - def _count_haproxy_processes(self, listener_list): + def _count_haproxy_processes(self, lb_list): num = 0 - for listener_id in listener_list: - if util.is_listener_running(listener_id): + for lb_id in lb_list: + if util.is_lb_running(lb_id): # optional check if it's still running num += 1 return num diff --git a/octavia/amphorae/backends/agent/api_server/keepalived.py b/octavia/amphorae/backends/agent/api_server/keepalived.py index b888f614a1..85ae429d85 100644 --- a/octavia/amphorae/backends/agent/api_server/keepalived.py +++ b/octavia/amphorae/backends/agent/api_server/keepalived.py @@ -22,7 +22,7 @@ from oslo_config import cfg from oslo_log import log as logging import webob -from octavia.amphorae.backends.agent.api_server import listener +from octavia.amphorae.backends.agent.api_server import loadbalancer from octavia.amphorae.backends.agent.api_server import util from octavia.common import constants as consts @@ -43,7 +43,7 @@ check_script_template = j2_env.get_template(consts.CHECK_SCRIPT_CONF) class Keepalived(object): def upload_keepalived_config(self): - stream = listener.Wrapped(flask.request.stream) + stream = loadbalancer.Wrapped(flask.request.stream) if not os.path.exists(util.keepalived_dir()): os.makedirs(util.keepalived_dir()) diff --git a/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py b/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py index 2e3ba0c99f..bdf002e9c6 100644 --- a/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py +++ b/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py @@ -25,10 +25,9 @@ from oslo_log import log as logging import webob from werkzeug import exceptions -from octavia.amphorae.backends.agent.api_server import listener +from octavia.amphorae.backends.agent.api_server import loadbalancer from octavia.amphorae.backends.agent.api_server import udp_listener_base from octavia.amphorae.backends.agent.api_server import util -from octavia.amphorae.backends.utils import keepalivedlvs_query from octavia.common import constants as consts BUFFER = 100 @@ -51,7 +50,7 @@ class KeepalivedLvs(udp_listener_base.UdpListenerApiServerBase): _SUBSCRIBED_AMP_COMPILE = ['keepalived', 'ipvsadm'] def upload_udp_listener_config(self, listener_id): - stream = listener.Wrapped(flask.request.stream) + stream = loadbalancer.Wrapped(flask.request.stream) NEED_CHECK = True if not os.path.exists(util.keepalived_lvs_dir()): @@ -256,45 +255,6 @@ class KeepalivedLvs(udp_listener_base.UdpListenerApiServerBase): }) return listeners - def get_udp_listener_status(self, listener_id): - """Gets the status of a UDP listener - - This method will consult the stats socket - so calling this method will interfere with - the health daemon with the risk of the amphora - shut down - - :param listener_id: The id of the listener - """ - self._check_udp_listener_exists(listener_id) - - status = self._check_udp_listener_status(listener_id) - - if status != consts.ACTIVE: - stats = dict( - status=status, - uuid=listener_id, - type='UDP' - ) - return webob.Response(json=stats) - - stats = dict( - status=status, - uuid=listener_id, - type='UDP' - ) - - try: - pool = keepalivedlvs_query.get_udp_listener_pool_status( - listener_id) - except subprocess.CalledProcessError as e: - return webob.Response(json=dict( - message="Error getting kernel lvs status for udp listener " - "{}".format(listener_id), - details=e.output), status=500) - stats['pools'] = [pool] - return webob.Response(json=stats) - def delete_udp_listener(self, listener_id): try: self._check_udp_listener_exists(listener_id) diff --git a/octavia/amphorae/backends/agent/api_server/listener.py b/octavia/amphorae/backends/agent/api_server/loadbalancer.py similarity index 67% rename from octavia/amphorae/backends/agent/api_server/listener.py rename to octavia/amphorae/backends/agent/api_server/loadbalancer.py index bbb423efa5..0930c8cabe 100644 --- a/octavia/amphorae/backends/agent/api_server/listener.py +++ b/octavia/amphorae/backends/agent/api_server/loadbalancer.py @@ -31,7 +31,6 @@ from werkzeug import exceptions from octavia.amphorae.backends.agent.api_server import haproxy_compatibility from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import util -from octavia.amphorae.backends.utils import haproxy_query as query from octavia.common import constants as consts from octavia.common import utils as octavia_utils @@ -54,10 +53,6 @@ SYSVINIT_TEMPLATE = JINJA_ENV.get_template(SYSVINIT_CONF) SYSTEMD_TEMPLATE = JINJA_ENV.get_template(SYSTEMD_CONF) -class ParsingError(Exception): - pass - - # Wrap a stream so we can compute the md5 while reading class Wrapped(object): def __init__(self, stream_): @@ -77,37 +72,37 @@ class Wrapped(object): return getattr(self.stream, attr) -class Listener(object): +class Loadbalancer(object): def __init__(self): self._osutils = osutils.BaseOS.get_os_util() - def get_haproxy_config(self, listener_id): + def get_haproxy_config(self, lb_id): """Gets the haproxy config :param listener_id: the id of the listener """ - self._check_listener_exists(listener_id) - with open(util.config_path(listener_id), 'r') as file: + self._check_lb_exists(lb_id) + with open(util.config_path(lb_id), 'r') as file: cfg = file.read() resp = webob.Response(cfg, content_type='text/plain') resp.headers['ETag'] = hashlib.md5(six.b(cfg)).hexdigest() # nosec return resp - def upload_haproxy_config(self, amphora_id, listener_id): + def upload_haproxy_config(self, amphora_id, lb_id): """Upload the haproxy config :param amphora_id: The id of the amphora to update - :param listener_id: The id of the listener + :param lb_id: The id of the loadbalancer """ stream = Wrapped(flask.request.stream) # We have to hash here because HAProxy has a string length limitation # in the configuration file "peer " lines peer_name = octavia_utils.base64_sha1_string(amphora_id).rstrip('=') - if not os.path.exists(util.haproxy_dir(listener_id)): - os.makedirs(util.haproxy_dir(listener_id)) + if not os.path.exists(util.haproxy_dir(lb_id)): + os.makedirs(util.haproxy_dir(lb_id)) - name = os.path.join(util.haproxy_dir(listener_id), 'haproxy.cfg.new') + name = os.path.join(util.haproxy_dir(lb_id), 'haproxy.cfg.new') flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC # mode 00600 mode = stat.S_IRUSR | stat.S_IWUSR @@ -148,7 +143,7 @@ class Listener(object): status=400) # file ok - move it - os.rename(name, util.config_path(listener_id)) + os.rename(name, util.config_path(lb_id)) try: @@ -156,7 +151,7 @@ class Listener(object): LOG.debug('Found init system: %s', init_system) - init_path = util.init_path(listener_id, init_system) + init_path = util.init_path(lb_id, init_system) if init_system == consts.INIT_SYSTEMD: template = SYSTEMD_TEMPLATE @@ -194,9 +189,9 @@ class Listener(object): text = template.render( peer_name=peer_name, - haproxy_pid=util.pid_path(listener_id), + haproxy_pid=util.pid_path(lb_id), haproxy_cmd=util.CONF.haproxy_amphora.haproxy_cmd, - haproxy_cfg=util.config_path(listener_id), + haproxy_cfg=util.config_path(lb_id), haproxy_user_group_cfg=consts.HAPROXY_USER_GROUP_CFG, respawn_count=util.CONF.haproxy_amphora.respawn_count, respawn_interval=(util.CONF.haproxy_amphora. @@ -212,25 +207,25 @@ class Listener(object): # Make sure the new service is enabled on boot if init_system == consts.INIT_SYSTEMD: util.run_systemctl_command( - consts.ENABLE, "haproxy-{list}".format(list=listener_id)) + consts.ENABLE, "haproxy-{lb_id}".format(lb_id=lb_id)) elif init_system == consts.INIT_SYSVINIT: try: subprocess.check_output(init_enable_cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - LOG.error("Failed to enable haproxy-%(list)s service: " - "%(err)s %(out)s", {'list': listener_id, 'err': e, + LOG.error("Failed to enable haproxy-%(lb_id)s service: " + "%(err)s %(out)s", {'lb_id': lb_id, 'err': e, 'out': e.output}) return webob.Response(json=dict( message="Error enabling haproxy-{0} service".format( - listener_id), details=e.output), status=500) + lb_id), details=e.output), status=500) res = webob.Response(json={'message': 'OK'}, status=202) res.headers['ETag'] = stream.get_md5() return res - def start_stop_listener(self, listener_id, action): + def start_stop_lb(self, lb_id, action): action = action.lower() if action not in [consts.AMP_ACTION_START, consts.AMP_ACTION_STOP, @@ -239,30 +234,30 @@ class Listener(object): message='Invalid Request', details="Unknown action: {0}".format(action)), status=400) - self._check_listener_exists(listener_id) + self._check_lb_exists(lb_id) # Since this script should be created at LB create time # we can check for this path to see if VRRP is enabled # on this amphora and not write the file if VRRP is not in use if os.path.exists(util.keepalived_check_script_path()): - self.vrrp_check_script_update(listener_id, action) + self.vrrp_check_script_update(lb_id, action) # HAProxy does not start the process when given a reload # so start it if haproxy is not already running if action == consts.AMP_ACTION_RELOAD: - if consts.OFFLINE == self._check_haproxy_status(listener_id): + if consts.OFFLINE == self._check_haproxy_status(lb_id): action = consts.AMP_ACTION_START - cmd = ("/usr/sbin/service haproxy-{listener_id} {action}".format( - listener_id=listener_id, action=action)) + cmd = ("/usr/sbin/service haproxy-{lb_id} {action}".format( + lb_id=lb_id, action=action)) try: subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: if b'Job is already running' not in e.output: LOG.debug( - "Failed to %(action)s haproxy-%(list)s service: %(err)s " - "%(out)s", {'action': action, 'list': listener_id, + "Failed to %(action)s haproxy-%(lb_id)s service: %(err)s " + "%(out)s", {'action': action, 'lb_id': lb_id, 'err': e, 'out': e.output}) return webob.Response(json=dict( message="Error {0}ing haproxy".format(action), @@ -271,40 +266,40 @@ class Listener(object): consts.AMP_ACTION_RELOAD]: return webob.Response(json=dict( message='OK', - details='Listener {listener_id} {action}ed'.format( - listener_id=listener_id, action=action)), status=202) + details='Listener {lb_id} {action}ed'.format( + lb_id=lb_id, action=action)), status=202) details = ( 'Configuration file is valid\n' - 'haproxy daemon for {0} started'.format(listener_id) + 'haproxy daemon for {0} started'.format(lb_id) ) return webob.Response(json=dict(message='OK', details=details), status=202) - def delete_listener(self, listener_id): + def delete_lb(self, lb_id): try: - self._check_listener_exists(listener_id) + self._check_lb_exists(lb_id) except exceptions.HTTPException: return webob.Response(json={'message': 'OK'}) # check if that haproxy is still running and if stop it - if os.path.exists(util.pid_path(listener_id)) and os.path.exists( - os.path.join('/proc', util.get_haproxy_pid(listener_id))): - cmd = "/usr/sbin/service haproxy-{0} stop".format(listener_id) + if os.path.exists(util.pid_path(lb_id)) and os.path.exists( + os.path.join('/proc', util.get_haproxy_pid(lb_id))): + cmd = "/usr/sbin/service haproxy-{0} stop".format(lb_id) try: subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: LOG.error("Failed to stop haproxy-%s service: %s %s", - listener_id, e, e.output) + lb_id, e, e.output) return webob.Response(json=dict( message="Error stopping haproxy", details=e.output), status=500) # parse config and delete stats socket try: - cfg = self._parse_haproxy_file(listener_id) - os.remove(cfg['stats_socket']) + stats_socket = util.parse_haproxy_file(lb_id)[0] + os.remove(stats_socket) except Exception: pass @@ -313,22 +308,22 @@ class Listener(object): # on this amphora and not write the file if VRRP is not in use if os.path.exists(util.keepalived_check_script_path()): self.vrrp_check_script_update( - listener_id, action=consts.AMP_ACTION_STOP) + lb_id, action=consts.AMP_ACTION_STOP) # delete the ssl files try: - shutil.rmtree(self._cert_dir(listener_id)) + shutil.rmtree(self._cert_dir(lb_id)) except Exception: pass # disable the service init_system = util.get_os_init_system() - init_path = util.init_path(listener_id, init_system) + init_path = util.init_path(lb_id, init_system) if init_system == consts.INIT_SYSTEMD: util.run_systemctl_command( - consts.DISABLE, "haproxy-{list}".format( - list=listener_id)) + consts.DISABLE, "haproxy-{lb_id}".format( + lb_id=lb_id)) elif init_system == consts.INIT_SYSVINIT: init_disable_cmd = "insserv -r {file}".format(file=init_path) elif init_system != consts.INIT_UPSTART: @@ -339,15 +334,15 @@ class Listener(object): subprocess.check_output(init_disable_cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - LOG.error("Failed to disable haproxy-%(list)s service: " - "%(err)s %(out)s", {'list': listener_id, 'err': e, + LOG.error("Failed to disable haproxy-%(lb_id)s service: " + "%(err)s %(out)s", {'lb_id': lb_id, 'err': e, 'out': e.output}) return webob.Response(json=dict( message="Error disabling haproxy-{0} service".format( - listener_id), details=e.output), status=500) + lb_id), details=e.output), status=500) # delete the directory + init script for that listener - shutil.rmtree(util.haproxy_dir(listener_id)) + shutil.rmtree(util.haproxy_dir(lb_id)) if os.path.exists(init_path): os.remove(init_path) @@ -364,68 +359,29 @@ class Listener(object): """ listeners = list() - for listener in util.get_listeners(): - status = self._check_listener_status(listener) - listener_type = '' + for lb in util.get_loadbalancers(): + stats_socket, listeners_on_lb = util.parse_haproxy_file(lb) - if status == consts.ACTIVE: - listener_type = self._parse_haproxy_file(listener)['mode'] - - listeners.append({ - 'status': status, - 'uuid': listener, - 'type': listener_type, - }) + for listener_id, listener in listeners_on_lb.items(): + listeners.append({ + 'status': consts.ACTIVE, + 'uuid': listener_id, + 'type': listener['mode'], + }) if other_listeners: listeners = listeners + other_listeners return webob.Response(json=listeners, content_type='application/json') - def get_listener_status(self, listener_id): - """Gets the status of a listener - - This method will consult the stats socket - so calling this method will interfere with - the health daemon with the risk of the amphora - shut down - - Currently type==SSL is not detected - :param listener_id: The id of the listener - """ - self._check_listener_exists(listener_id) - - status = self._check_listener_status(listener_id) - - if status != consts.ACTIVE: - stats = dict( - status=status, - uuid=listener_id, - type='' - ) - return webob.Response(json=stats) - - cfg = self._parse_haproxy_file(listener_id) - stats = dict( - status=status, - uuid=listener_id, - type=cfg['mode'] - ) - - # read stats socket - q = query.HAProxyQuery(cfg['stats_socket']) - servers = q.get_pool_status() - stats['pools'] = list(servers.values()) - return webob.Response(json=stats) - - def upload_certificate(self, listener_id, filename): + def upload_certificate(self, lb_id, filename): self._check_ssl_filename_format(filename) # create directory if not already there - if not os.path.exists(self._cert_dir(listener_id)): - os.makedirs(self._cert_dir(listener_id)) + if not os.path.exists(self._cert_dir(lb_id)): + os.makedirs(self._cert_dir(lb_id)) stream = Wrapped(flask.request.stream) - file = self._cert_file_path(listener_id, filename) + file = self._cert_file_path(lb_id, filename) flags = os.O_WRONLY | os.O_CREAT # mode 00600 mode = stat.S_IRUSR | stat.S_IWUSR @@ -439,10 +395,10 @@ class Listener(object): resp.headers['ETag'] = stream.get_md5() return resp - def get_certificate_md5(self, listener_id, filename): + def get_certificate_md5(self, lb_id, filename): self._check_ssl_filename_format(filename) - cert_path = self._cert_file_path(listener_id, filename) + cert_path = self._cert_file_path(lb_id, filename) path_exists = os.path.exists(cert_path) if not path_exists: return webob.Response(json=dict( @@ -457,60 +413,34 @@ class Listener(object): resp.headers['ETag'] = md5 return resp - def delete_certificate(self, listener_id, filename): + def delete_certificate(self, lb_id, filename): self._check_ssl_filename_format(filename) - if os.path.exists(self._cert_file_path(listener_id, filename)): - os.remove(self._cert_file_path(listener_id, filename)) + if os.path.exists(self._cert_file_path(lb_id, filename)): + os.remove(self._cert_file_path(lb_id, filename)) return webob.Response(json=dict(message='OK')) - def _check_listener_status(self, listener_id): - if os.path.exists(util.pid_path(listener_id)): + def _get_listeners_on_lb(self, lb_id): + if os.path.exists(util.pid_path(lb_id)): if os.path.exists( - os.path.join('/proc', util.get_haproxy_pid(listener_id))): + os.path.join('/proc', util.get_haproxy_pid(lb_id))): # Check if the listener is disabled - with open(util.config_path(listener_id), 'r') as file: + with open(util.config_path(lb_id), 'r') as file: cfg = file.read() - m = re.search('frontend {}'.format(listener_id), cfg) - if m: - return consts.ACTIVE - return consts.OFFLINE + m = re.findall('^frontend (.*)$', cfg, re.MULTILINE) + return m or [] else: # pid file but no process... - return consts.ERROR + return [] else: - return consts.OFFLINE + return [] - def _parse_haproxy_file(self, listener_id): - with open(util.config_path(listener_id), 'r') as file: - cfg = file.read() - - m = re.search(r'mode\s+(http|tcp)', cfg) - if not m: - raise ParsingError() - mode = m.group(1).upper() - - m = re.search(r'stats socket\s+(\S+)', cfg) - if not m: - raise ParsingError() - stats_socket = m.group(1) - - m = re.search(r'ssl crt\s+(\S+)', cfg) - ssl_crt = None - if m: - ssl_crt = m.group(1) - mode = 'TERMINATED_HTTPS' - - return dict(mode=mode, - stats_socket=stats_socket, - ssl_crt=ssl_crt) - - def _check_listener_exists(self, listener_id): - # check if we know about that listener - if not os.path.exists(util.config_path(listener_id)): + def _check_lb_exists(self, lb_id): + # check if we know about that lb + if lb_id not in util.get_loadbalancers(): raise exceptions.HTTPException( response=webob.Response(json=dict( - message='Listener Not Found', - details="No listener with UUID: {0}".format( - listener_id)), status=404)) + message='Loadbalancer Not Found', + details="No loadbalancer with UUID: {0}".format( + lb_id)), status=404)) def _check_ssl_filename_format(self, filename): # check if the format is (xxx.)*xxx.pem @@ -519,20 +449,19 @@ class Listener(object): response=webob.Response(json=dict( message='Filename has wrong format'), status=400)) - def _cert_dir(self, listener_id): - return os.path.join(util.CONF.haproxy_amphora.base_cert_dir, - listener_id) + def _cert_dir(self, lb_id): + return os.path.join(util.CONF.haproxy_amphora.base_cert_dir, lb_id) - def _cert_file_path(self, listener_id, filename): - return os.path.join(self._cert_dir(listener_id), filename) + def _cert_file_path(self, lb_id, filename): + return os.path.join(self._cert_dir(lb_id), filename) - def vrrp_check_script_update(self, listener_id, action): - listener_ids = util.get_listeners() + def vrrp_check_script_update(self, lb_id, action): + lb_ids = util.get_loadbalancers() if action == consts.AMP_ACTION_STOP: - listener_ids.remove(listener_id) + lb_ids.remove(lb_id) args = [] - for lid in listener_ids: - args.append(util.haproxy_sock_path(lid)) + for lbid in lb_ids: + args.append(util.haproxy_sock_path(lbid)) if not os.path.exists(util.keepalived_dir()): os.makedirs(util.keepalived_dir()) @@ -542,9 +471,9 @@ class Listener(object): with open(util.haproxy_check_script_path(), 'w') as text_file: text_file.write(cmd) - def _check_haproxy_status(self, listener_id): - if os.path.exists(util.pid_path(listener_id)): + def _check_haproxy_status(self, lb_id): + if os.path.exists(util.pid_path(lb_id)): if os.path.exists( - os.path.join('/proc', util.get_haproxy_pid(listener_id))): + os.path.join('/proc', util.get_haproxy_pid(lb_id))): return consts.ACTIVE return consts.OFFLINE diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index de5b77a105..cb45499a33 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -26,7 +26,7 @@ from octavia.amphorae.backends.agent import api_server from octavia.amphorae.backends.agent.api_server import amphora_info from octavia.amphorae.backends.agent.api_server import certificate_update from octavia.amphorae.backends.agent.api_server import keepalived -from octavia.amphorae.backends.agent.api_server import listener +from octavia.amphorae.backends.agent.api_server import loadbalancer from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import plug from octavia.amphorae.backends.agent.api_server import udp_listener_base @@ -56,7 +56,7 @@ class Server(object): self.app = flask.Flask(__name__) self._osutils = osutils.BaseOS.get_os_util() self._keepalived = keepalived.Keepalived() - self._listener = listener.Listener() + self._loadbalancer = loadbalancer.Loadbalancer() self._udp_listener = (udp_listener_base.UdpListenerApiServerBase. get_server_driver()) self._plug = plug.Plug(self._osutils) @@ -64,8 +64,10 @@ class Server(object): register_app_error_handler(self.app) + self.app.add_url_rule(rule='/', view_func=self.version_discovery, + methods=['GET']) self.app.add_url_rule(rule=PATH_PREFIX + - '/listeners///haproxy', + '/loadbalancer///haproxy', view_func=self.upload_haproxy_config, methods=['PUT']) self.app.add_url_rule(rule=PATH_PREFIX + @@ -74,7 +76,7 @@ class Server(object): view_func=self.upload_udp_listener_config, methods=['PUT']) self.app.add_url_rule(rule=PATH_PREFIX + - '/listeners//haproxy', + '/loadbalancer//haproxy', view_func=self.get_haproxy_config, methods=['GET']) self.app.add_url_rule(rule=PATH_PREFIX + @@ -82,11 +84,11 @@ class Server(object): view_func=self.get_udp_listener_config, methods=['GET']) self.app.add_url_rule(rule=PATH_PREFIX + - '/listeners//', - view_func=self.start_stop_listener, + '/loadbalancer//', + view_func=self.start_stop_lb_object, methods=['PUT']) - self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/', - view_func=self.delete_listener, + self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/', + view_func=self.delete_lb_object, methods=['DELETE']) self.app.add_url_rule(rule=PATH_PREFIX + '/config', view_func=self.upload_config, @@ -100,18 +102,15 @@ class Server(object): self.app.add_url_rule(rule=PATH_PREFIX + '/listeners', view_func=self.get_all_listeners_status, methods=['GET']) - self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/', - view_func=self.get_listener_status, - methods=['GET']) - self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/' + self.app.add_url_rule(rule=PATH_PREFIX + '/loadbalancer/' '/certificates/', view_func=self.upload_certificate, methods=['PUT']) - self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/' + self.app.add_url_rule(rule=PATH_PREFIX + '/loadbalancer/' '/certificates/', view_func=self.get_certificate_md5, methods=['GET']) - self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/' + self.app.add_url_rule(rule=PATH_PREFIX + '/loadbalancer/' '/certificates/', view_func=self.delete_certificate, methods=['DELETE']) @@ -133,30 +132,30 @@ class Server(object): view_func=self.get_interface, methods=['GET']) - def upload_haproxy_config(self, amphora_id, listener_id): - return self._listener.upload_haproxy_config(amphora_id, listener_id) + def upload_haproxy_config(self, amphora_id, lb_id): + return self._loadbalancer.upload_haproxy_config(amphora_id, lb_id) def upload_udp_listener_config(self, amphora_id, listener_id): return self._udp_listener.upload_udp_listener_config(listener_id) - def get_haproxy_config(self, listener_id): - return self._listener.get_haproxy_config(listener_id) + def get_haproxy_config(self, lb_id): + return self._loadbalancer.get_haproxy_config(lb_id) def get_udp_listener_config(self, listener_id): return self._udp_listener.get_udp_listener_config(listener_id) - def start_stop_listener(self, listener_id, action): - protocol = util.get_listener_protocol(listener_id) + def start_stop_lb_object(self, object_id, action): + protocol = util.get_protocol_for_lb_object(object_id) if protocol == 'UDP': return self._udp_listener.manage_udp_listener( - listener_id, action) - return self._listener.start_stop_listener(listener_id, action) + listener_id=object_id, action=action) + return self._loadbalancer.start_stop_lb(lb_id=object_id, action=action) - def delete_listener(self, listener_id): - protocol = util.get_listener_protocol(listener_id) + def delete_lb_object(self, object_id): + protocol = util.get_protocol_for_lb_object(object_id) if protocol == 'UDP': - return self._udp_listener.delete_udp_listener(listener_id) - return self._listener.delete_listener(listener_id) + return self._udp_listener.delete_udp_listener(object_id) + return self._loadbalancer.delete_lb(object_id) def get_details(self): return self._amphora_info.compile_amphora_details( @@ -168,23 +167,17 @@ class Server(object): def get_all_listeners_status(self): udp_listeners = self._udp_listener.get_all_udp_listeners_status() - return self._listener.get_all_listeners_status( + return self._loadbalancer.get_all_listeners_status( other_listeners=udp_listeners) - def get_listener_status(self, listener_id): - protocol = util.get_listener_protocol(listener_id) - if protocol == 'UDP': - return self._udp_listener.get_udp_listener_status(listener_id) - return self._listener.get_listener_status(listener_id) + def upload_certificate(self, lb_id, filename): + return self._loadbalancer.upload_certificate(lb_id, filename) - def upload_certificate(self, listener_id, filename): - return self._listener.upload_certificate(listener_id, filename) + def get_certificate_md5(self, lb_id, filename): + return self._loadbalancer.get_certificate_md5(lb_id, filename) - def get_certificate_md5(self, listener_id, filename): - return self._listener.get_certificate_md5(listener_id, filename) - - def delete_certificate(self, listener_id, filename): - return self._listener.delete_certificate(listener_id, filename) + def delete_certificate(self, lb_id, filename): + return self._loadbalancer.delete_certificate(lb_id, filename) def plug_vip(self, vip): # Catch any issues with the subnet info json @@ -251,3 +244,6 @@ class Server(object): details=str(e)), status=500) return webob.Response(json={'message': 'OK'}, status=202) + + def version_discovery(self): + return webob.Response(json={'api_version': api_server.VERSION}) diff --git a/octavia/amphorae/backends/agent/api_server/udp_listener_base.py b/octavia/amphorae/backends/agent/api_server/udp_listener_base.py index 60edb883f2..e60ba937a7 100644 --- a/octavia/amphorae/backends/agent/api_server/udp_listener_base.py +++ b/octavia/amphorae/backends/agent/api_server/udp_listener_base.py @@ -94,17 +94,6 @@ class UdpListenerApiServerBase(object): """ - @abc.abstractmethod - def get_udp_listener_status(self, listener_id): - """Gets the status of a UDP listener - - :param listener_id: The id of the listener - - :returns: HTTP response with status code. - :raises Exception: If the listener is failed to find. - - """ - @abc.abstractmethod def delete_udp_listener(self, listener_id): """Delete a UDP Listener from a amphora diff --git a/octavia/amphorae/backends/agent/api_server/util.py b/octavia/amphorae/backends/agent/api_server/util.py index 7f20635a7d..fef833d3fc 100644 --- a/octavia/amphorae/backends/agent/api_server/util.py +++ b/octavia/amphorae/backends/agent/api_server/util.py @@ -28,21 +28,31 @@ from octavia.common import constants as consts CONF = cfg.CONF LOG = logging.getLogger(__name__) +FRONTEND_BACKEND_PATTERN = re.compile(r'\n(frontend|backend)\s+(\S+)\n') +LISTENER_MODE_PATTERN = re.compile(r'^\s+mode\s+(.*)$', re.MULTILINE) +TLS_CERT_PATTERN = re.compile(r'^\s+bind\s+\S+\s+ssl crt\s+(.*)$', + re.MULTILINE) +STATS_SOCKET_PATTERN = re.compile(r'stats socket\s+(\S+)') + + +class ParsingError(Exception): + pass + class UnknownInitError(Exception): pass -def init_path(listener_id, init_system): +def init_path(lb_id, init_system): if init_system == consts.INIT_SYSTEMD: return os.path.join(consts.SYSTEMD_DIR, - 'haproxy-{0}.service'.format(listener_id)) + 'haproxy-{0}.service'.format(lb_id)) if init_system == consts.INIT_UPSTART: return os.path.join(consts.UPSTART_DIR, - 'haproxy-{0}.conf'.format(listener_id)) + 'haproxy-{0}.conf'.format(lb_id)) if init_system == consts.INIT_SYSVINIT: return os.path.join(consts.SYSVINIT_DIR, - 'haproxy-{0}'.format(listener_id)) + 'haproxy-{0}'.format(lb_id)) raise UnknownInitError() @@ -91,20 +101,20 @@ def keepalived_lvs_cfg_path(listener_id): str(listener_id)) -def haproxy_dir(listener_id): - return os.path.join(CONF.haproxy_amphora.base_path, listener_id) +def haproxy_dir(lb_id): + return os.path.join(CONF.haproxy_amphora.base_path, lb_id) -def pid_path(listener_id): - return os.path.join(haproxy_dir(listener_id), listener_id + '.pid') +def pid_path(lb_id): + return os.path.join(haproxy_dir(lb_id), lb_id + '.pid') -def config_path(listener_id): - return os.path.join(haproxy_dir(listener_id), 'haproxy.cfg') +def config_path(lb_id): + return os.path.join(haproxy_dir(lb_id), 'haproxy.cfg') -def get_haproxy_pid(listener_id): - with open(pid_path(listener_id), 'r') as f: +def get_haproxy_pid(lb_id): + with open(pid_path(lb_id), 'r') as f: return f.readline().rstrip() @@ -114,8 +124,8 @@ def get_keepalivedlvs_pid(listener_id): return f.readline().rstrip() -def haproxy_sock_path(listener_id): - return os.path.join(CONF.haproxy_amphora.base_path, listener_id + '.sock') +def haproxy_sock_path(lb_id): + return os.path.join(CONF.haproxy_amphora.base_path, lb_id + '.sock') def haproxy_check_script_path(): @@ -168,16 +178,28 @@ def get_listeners(): :returns: An array with the ids of all listeners, e.g. ['123', '456', ...] or [] if no listeners exist """ + listeners = [] + for lb_id in get_loadbalancers(): + listeners_on_lb = parse_haproxy_file(lb_id)[1] + listeners.extend(list(listeners_on_lb.keys())) + return listeners + +def get_loadbalancers(): + """Get Load balancers + + :returns: An array with the ids of all load balancers, + e.g. ['123', '456', ...] or [] if no loadbalancers exist + """ if os.path.exists(CONF.haproxy_amphora.base_path): return [f for f in os.listdir(CONF.haproxy_amphora.base_path) if os.path.exists(config_path(f))] return [] -def is_listener_running(listener_id): - return os.path.exists(pid_path(listener_id)) and os.path.exists( - os.path.join('/proc', get_haproxy_pid(listener_id))) +def is_lb_running(lb_id): + return os.path.exists(pid_path(lb_id)) and os.path.exists( + os.path.join('/proc', get_haproxy_pid(lb_id))) def get_udp_listeners(): @@ -251,7 +273,7 @@ def run_systemctl_command(command, service): 'err': e, 'out': e.output}) -def get_listener_protocol(listener_id): +def get_protocol_for_lb_object(object_id): """Returns the L4 protocol for a listener. If the listener is a TCP based listener (haproxy) return TCP. @@ -261,8 +283,52 @@ def get_listener_protocol(listener_id): :param listener_id: The ID of the listener to identify. :returns: TCP, UDP, or None """ - if os.path.exists(config_path(listener_id)): + if os.path.exists(config_path(object_id)): return consts.PROTOCOL_TCP - if os.path.exists(keepalived_lvs_cfg_path(listener_id)): + if os.path.exists(keepalived_lvs_cfg_path(object_id)): return consts.PROTOCOL_UDP return None + + +def parse_haproxy_file(lb_id): + with open(config_path(lb_id), 'r') as file: + cfg = file.read() + + listeners = {} + + m = FRONTEND_BACKEND_PATTERN.split(cfg) + last_token = None + last_id = None + for section in m: + if last_token is None: + # We aren't in a section yet, see if this line starts one + if section == 'frontend': + last_token = section + elif last_token == 'frontend': + # We're in a frontend section, save the id for later + last_token = last_token + "_id" + last_id = section + elif last_token == 'frontend_id': + # We're in a frontend section and already have the id + # Look for the mode + mode_matcher = LISTENER_MODE_PATTERN.search(section) + if not mode_matcher: + raise ParsingError() + listeners[last_id] = { + 'mode': mode_matcher.group(1).upper(), + } + # Now see if this is a TLS frontend + tls_matcher = TLS_CERT_PATTERN.search(section) + if tls_matcher: + # TODO(rm_work): Can't we have terminated tcp? + listeners[last_id]['mode'] = 'TERMINATED_HTTPS' + listeners[last_id]['ssl_crt'] = tls_matcher.group(1) + # Clear out the token and id and start over + last_token = last_id = None + + m = STATS_SOCKET_PATTERN.search(cfg) + if not m: + raise ParsingError() + stats_socket = m.group(1) + + return stats_socket, listeners diff --git a/octavia/amphorae/backends/health_daemon/health_daemon.py b/octavia/amphorae/backends/health_daemon/health_daemon.py index 930ac0ad7e..ccabe9c3ca 100644 --- a/octavia/amphorae/backends/health_daemon/health_daemon.py +++ b/octavia/amphorae/backends/health_daemon/health_daemon.py @@ -43,18 +43,19 @@ SEQ = 0 # incompatible changes. # # ver 1 - Adds UDP listener status when no pool or members are present +# ver 2 - Switch to all listeners in a single combined haproxy config # -MSG_VER = 1 +MSG_VER = 2 def list_sock_stat_files(hadir=None): stat_sock_files = {} if hadir is None: hadir = CONF.haproxy_amphora.base_path - listener_ids = util.get_listeners() - for listener_id in listener_ids: - sock_file = listener_id + ".sock" - stat_sock_files[listener_id] = os.path.join(hadir, sock_file) + lb_ids = util.get_loadbalancers() + for lb_id in lb_ids: + sock_file = lb_id + ".sock" + stat_sock_files[lb_id] = os.path.join(hadir, sock_file) return stat_sock_files @@ -115,39 +116,55 @@ def get_stats(stat_sock_file): def build_stats_message(): + # Example version 2 message without UDP: + # { + # "id": "", + # "seq": 67, + # "listeners": { + # "": { + # "status": "OPEN", + # "stats": { + # "tx": 0, + # "rx": 0, + # "conns": 0, + # "totconns": 0, + # "ereq": 0 + # } + # } + # }, + # "pools": { + # ":": { + # "status": "UP", + # "members": { + # "": "no check" + # } + # } + # }, + # "ver": 2 + # } global SEQ msg = {'id': CONF.amphora_agent.amphora_id, - 'seq': SEQ, "listeners": {}, + 'seq': SEQ, 'listeners': {}, 'pools': {}, 'ver': MSG_VER} SEQ += 1 stat_sock_files = list_sock_stat_files() - for listener_id, stat_sock_file in stat_sock_files.items(): - listener_dict = {'pools': {}, - 'status': 'DOWN', - 'stats': { - 'tx': 0, - 'rx': 0, - 'conns': 0, - 'totconns': 0, - 'ereq': 0}} - msg['listeners'][listener_id] = listener_dict - if util.is_listener_running(listener_id): + # TODO(rm_work) There should only be one of these in the new config system + for lb_id, stat_sock_file in stat_sock_files.items(): + if util.is_lb_running(lb_id): (stats, pool_status) = get_stats(stat_sock_file) - listener_dict = msg['listeners'][listener_id] for row in stats: if row['svname'] == 'FRONTEND': - listener_dict['stats']['tx'] = int(row['bout']) - listener_dict['stats']['rx'] = int(row['bin']) - listener_dict['stats']['conns'] = int(row['scur']) - listener_dict['stats']['totconns'] = int(row['stot']) - listener_dict['stats']['ereq'] = int(row['ereq']) - listener_dict['status'] = row['status'] - for oid, pool in pool_status.items(): - if oid != listener_id: - pool_id = oid - pools = listener_dict['pools'] - pools[pool_id] = {"status": pool['status'], - "members": pool['members']} + listener_id = row['pxname'] + msg['listeners'][listener_id] = { + 'status': row['status'], + 'stats': {'tx': int(row['bout']), + 'rx': int(row['bin']), + 'conns': int(row['scur']), + 'totconns': int(row['stot']), + 'ereq': int(row['ereq'])}} + for pool_id, pool in pool_status.items(): + msg['pools'][pool_id] = {"status": pool['status'], + "members": pool['members']} # UDP listener part udp_listener_ids = util.get_udp_listeners() diff --git a/octavia/amphorae/backends/utils/haproxy_query.py b/octavia/amphorae/backends/utils/haproxy_query.py index 7632481932..cd503412a6 100644 --- a/octavia/amphorae/backends/utils/haproxy_query.py +++ b/octavia/amphorae/backends/utils/haproxy_query.py @@ -124,7 +124,9 @@ class HAProxyQuery(object): final_results[line['pxname']] = dict(members={}) if line['svname'] == 'BACKEND': - final_results[line['pxname']]['uuid'] = line['pxname'] + pool_id, listener_id = line['pxname'].split(':') + final_results[line['pxname']]['pool_uuid'] = pool_id + final_results[line['pxname']]['listener_uuid'] = listener_id final_results[line['pxname']]['status'] = line['status'] else: final_results[line['pxname']]['members'][line['svname']] = ( diff --git a/octavia/amphorae/drivers/driver_base.py b/octavia/amphorae/drivers/driver_base.py index a2aa09bc06..6e695b8e0a 100644 --- a/octavia/amphorae/drivers/driver_base.py +++ b/octavia/amphorae/drivers/driver_base.py @@ -22,17 +22,19 @@ import six class AmphoraLoadBalancerDriver(object): @abc.abstractmethod - def update_amphora_listeners(self, listeners, amphora_id, timeout_dict): + def update_amphora_listeners(self, loadbalancer, amphora, + timeout_dict): """Update the amphora with a new configuration. - :param listeners: List of listeners to update. - :type listener: list - :param amphora_id: The ID of the amphora to update - :type amphora_id: string + :param loadbalancer: List of listeners to update. + :type loadbalancer: list(octavia.db.models.Listener) + :param amphora: The index of the specific amphora to update + :type amphora: octavia.db.models.Amphora :param timeout_dict: Dictionary of timeout values for calls to the amphora. May contain: req_conn_timeout, req_read_timeout, conn_max_retries, conn_retry_interval + :type timeout_dict: dict :returns: None Builds a new configuration, pushes it to the amphora, and reloads @@ -40,14 +42,12 @@ class AmphoraLoadBalancerDriver(object): """ @abc.abstractmethod - def update(self, listener, vip): + def update(self, loadbalancer): """Update the amphora with a new configuration. - :param listener: listener object, - need to use its protocol_port property - :type listener: object - :param vip: vip object, need to use its ip_address property - :type vip: object + :param loadbalancer: loadbalancer object, need to use its + vip.ip_address property + :type loadbalancer: octavia.db.models.LoadBalancer :returns: None At this moment, we just build the basic structure for testing, will @@ -55,31 +55,13 @@ class AmphoraLoadBalancerDriver(object): """ @abc.abstractmethod - def stop(self, listener, vip): - """Stop the listener on the vip. + def start(self, loadbalancer, amphora): + """Start the listeners on the amphora. - :param listener: listener object, - need to use its protocol_port property - :type listener: object - :param vip: vip object, need to use its ip_address property - :type vip: object - :returns: return a value list (listener, vip, status flag--suspend) - - At this moment, we just build the basic structure for testing, will - add more function along with the development. - """ - - @abc.abstractmethod - def start(self, listener, vip, amphora): - """Start the listener on the vip. - - :param listener: listener object, - need to use its protocol_port property - :type listener: object - :param vip: vip object, need to use its ip_address property - :type vip: object + :param loadbalancer: loadbalancer object to start listeners + :type loadbalancer: octavia.db.models.LoadBalancer :param amphora: Amphora to start. If None, start on all amphora - :type amphora: object + :type amphora: octavia.db.models.Amphora :returns: return a value list (listener, vip, status flag--enable) At this moment, we just build the basic structure for testing, will @@ -87,14 +69,12 @@ class AmphoraLoadBalancerDriver(object): """ @abc.abstractmethod - def delete(self, listener, vip): + def delete(self, listener): """Delete the listener on the vip. :param listener: listener object, need to use its protocol_port property - :type listener: object - :param vip: vip object, need to use its ip_address property - :type vip: object + :type listener: octavia.db.models.Listener :returns: return a value list (listener, vip, status flag--delete) At this moment, we just build the basic structure for testing, will @@ -106,7 +86,7 @@ class AmphoraLoadBalancerDriver(object): """Returns information about the amphora. :param amphora: amphora object, need to use its id property - :type amphora: object + :type amphora: octavia.db.models.Amphora :returns: return a value list (amphora.id, status flag--'info') At this moment, we just build the basic structure for testing, will @@ -122,7 +102,7 @@ class AmphoraLoadBalancerDriver(object): """Return ceilometer ready diagnostic data. :param amphora: amphora object, need to use its id property - :type amphora: object + :type amphora: octavia.db.models.Amphora :returns: return a value list (amphora.id, status flag--'ge t_diagnostics') @@ -138,7 +118,7 @@ class AmphoraLoadBalancerDriver(object): """Finalize the amphora before any listeners are configured. :param amphora: amphora object, need to use its id property - :type amphora: object + :type amphora: octavia.db.models.Amphora :returns: None At this moment, we just build the basic structure for testing, will @@ -151,6 +131,8 @@ class AmphoraLoadBalancerDriver(object): def post_vip_plug(self, amphora, load_balancer, amphorae_network_config): """Called after network driver has allocated and plugged the VIP + :param amphora: + :type amphora: octavia.db.models.Amphora :param load_balancer: A load balancer that just had its vip allocated and plugged in the network driver. :type load_balancer: octavia.common.data_models.LoadBalancer @@ -168,7 +150,7 @@ class AmphoraLoadBalancerDriver(object): """Called after amphora added to network :param amphora: amphora object, needs id and network ip(s) - :type amphora: object + :type amphora: octavia.db.models.Amphora :param port: contains information of the plugged port :type port: octavia.network.data_models.Port @@ -182,7 +164,7 @@ class AmphoraLoadBalancerDriver(object): """Start health checks. :param health_mixin: health mixin object - :type amphora: object + :type health_mixin: HealthMixin Starts listener process and calls HealthMixin to update databases information. @@ -199,7 +181,7 @@ class AmphoraLoadBalancerDriver(object): """Upload cert info to the amphora. :param amphora: amphora object, needs id and network ip(s) - :type amphora: object + :type amphora: octavia.db.models.Amphora :param pem_file: a certificate file :type pem_file: file object @@ -210,7 +192,7 @@ class AmphoraLoadBalancerDriver(object): """Upload and update the amphora agent configuration. :param amphora: amphora object, needs id and network ip(s) - :type amphora: object + :type amphora: octavia.db.models.Amphora :param agent_config: The new amphora agent configuration file. :type agent_config: string """ diff --git a/octavia/amphorae/drivers/haproxy/rest_api_driver.py b/octavia/amphorae/drivers/haproxy/rest_api_driver.py index 5f1d6263d8..4887d4828b 100644 --- a/octavia/amphorae/drivers/haproxy/rest_api_driver.py +++ b/octavia/amphorae/drivers/haproxy/rest_api_driver.py @@ -32,7 +32,8 @@ from octavia.amphorae.drivers.haproxy import exceptions as exc from octavia.amphorae.drivers.keepalived import vrrp_rest_driver from octavia.common.config import cfg from octavia.common import constants as consts -from octavia.common.jinja.haproxy import jinja_cfg +import octavia.common.jinja.haproxy.combined_listeners.jinja_cfg as jinja_combo +import octavia.common.jinja.haproxy.split_listeners.jinja_cfg as jinja_split from octavia.common.jinja.lvs import jinja_cfg as jinja_udp_cfg from octavia.common.tls_utils import cert_parser from octavia.common import utils @@ -51,14 +52,23 @@ class HaproxyAmphoraLoadBalancerDriver( def __init__(self): super(HaproxyAmphoraLoadBalancerDriver, self).__init__() - self.client = AmphoraAPIClient() + self.clients = { + 'base': AmphoraAPIClientBase(), + '0.5': AmphoraAPIClient0_5(), + '1.0': AmphoraAPIClient1_0(), + } self.cert_manager = stevedore_driver.DriverManager( namespace='octavia.cert_manager', name=CONF.certificates.cert_manager, invoke_on_load=True, ).driver - self.jinja = jinja_cfg.JinjaTemplater( + self.jinja_combo = jinja_combo.JinjaTemplater( + base_amp_path=CONF.haproxy_amphora.base_path, + base_crt_dir=CONF.haproxy_amphora.base_cert_dir, + haproxy_template=CONF.haproxy_amphora.haproxy_template, + connection_logging=CONF.haproxy_amphora.connection_logging) + self.jinja_split = jinja_split.JinjaTemplater( base_amp_path=CONF.haproxy_amphora.base_path, base_crt_dir=CONF.haproxy_amphora.base_cert_dir, haproxy_template=CONF.haproxy_amphora.haproxy_template, @@ -72,21 +82,39 @@ class HaproxyAmphoraLoadBalancerDriver( :returns version_list: A list with the major and minor numbers """ + self._populate_amphora_api_version(amphora) + amp_info = self.clients[amphora.api_version].get_info(amphora) + haproxy_version_string = amp_info['haproxy_version'] - version_string = self.client.get_info(amphora)['haproxy_version'] + return haproxy_version_string.split('.')[:2] - return version_string.split('.')[:2] + def _populate_amphora_api_version(self, amphora): + """Populate the amphora object with the api_version - def update_amphora_listeners(self, listeners, amphora_index, - amphorae, timeout_dict=None): + This will query the amphora for version discovery and populate + the api_version string attribute on the amphora object. + + :returns: None + """ + if not getattr(amphora, 'api_version', None): + try: + amphora.api_version = self.clients['base'].get_api_version( + amphora)['api_version'] + except exc.NotFound: + # Amphora is too old for version discovery, default to 0.5 + amphora.api_version = '0.5' + LOG.debug('Amphora %s has API version %s', + amphora.id, amphora.api_version) + return list(map(int, amphora.api_version.split('.'))) + + def update_amphora_listeners(self, loadbalancer, amphora, + timeout_dict=None): """Update the amphora with a new configuration. - :param listeners: List of listeners to update. - :type listener: list - :param amphora_index: The index of the amphora to update - :type amphora_index: integer - :param amphorae: List of amphorae - :type amphorae: list + :param loadbalancer: The load balancer to update + :type loadbalancer: object + :param amphora: The amphora to update + :type amphora: object :param timeout_dict: Dictionary of timeout values for calls to the amphora. May contain: req_conn_timeout, req_read_timeout, conn_max_retries, @@ -96,46 +124,82 @@ class HaproxyAmphoraLoadBalancerDriver( Updates the configuration of the listeners on a single amphora. """ # if the amphora does not yet have listeners, no need to update them. - if not listeners: + if not loadbalancer.listeners: LOG.debug('No listeners found to update.') return - amp = amphorae[amphora_index] - if amp is None or amp.status == consts.DELETED: + if amphora is None or amphora.status == consts.DELETED: return - haproxy_versions = self._get_haproxy_versions(amp) + # Check which HAProxy version is on the amp + haproxy_versions = self._get_haproxy_versions(amphora) + # Check which config style to use + api_version = self._populate_amphora_api_version(amphora) + if api_version[0] == 0 and api_version[1] <= 5: # 0.5 or earlier + split_config = True + LOG.warning( + 'Amphora %s for loadbalancer %s needs upgrade to single ' + 'process mode.', amphora.id, loadbalancer.id) + else: + split_config = False + LOG.debug('Amphora %s for loadbalancer %s is already in single ' + 'process mode.', amphora.id, loadbalancer.id) - # TODO(johnsom) remove when we don't have a process per listener - for listener in listeners: + has_tcp = False + for listener in loadbalancer.listeners: LOG.debug("%s updating listener %s on amphora %s", - self.__class__.__name__, listener.id, amp.id) + self.__class__.__name__, listener.id, amphora.id) if listener.protocol == 'UDP': # Generate Keepalived LVS configuration from listener object config = self.udp_jinja.build_config(listener=listener) - self.client.upload_udp_config(amp, listener.id, config, - timeout_dict=timeout_dict) - self.client.reload_listener(amp, listener.id, - timeout_dict=timeout_dict) + self.clients[amphora.api_version].upload_udp_config( + amphora, listener.id, config, timeout_dict=timeout_dict) + self.clients[amphora.api_version].reload_listener( + amphora, listener.id, timeout_dict=timeout_dict) else: - certs = self._process_tls_certificates(listener) - client_ca_filename = self._process_secret( - listener, listener.client_ca_tls_certificate_id) - crl_filename = self._process_secret( - listener, listener.client_crl_container_id) - pool_tls_certs = self._process_listener_pool_certs(listener) + has_tcp = True + if split_config: + obj_id = listener.id + else: + obj_id = loadbalancer.id - # Generate HaProxy configuration from listener object - config = self.jinja.build_config( - host_amphora=amp, listener=listener, - tls_cert=certs['tls_cert'], - haproxy_versions=haproxy_versions, - client_ca_filename=client_ca_filename, - client_crl=crl_filename, - pool_tls_certs=pool_tls_certs) - self.client.upload_config(amp, listener.id, config, - timeout_dict=timeout_dict) - self.client.reload_listener(amp, listener.id, - timeout_dict=timeout_dict) + certs = self._process_tls_certificates( + listener, amphora, obj_id) + client_ca_filename = self._process_secret( + listener, listener.client_ca_tls_certificate_id, + amphora, obj_id) + crl_filename = self._process_secret( + listener, listener.client_crl_container_id, + amphora, obj_id) + pool_tls_certs = self._process_listener_pool_certs( + listener, amphora, obj_id) + + if split_config: + config = self.jinja_split.build_config( + host_amphora=amphora, listener=listener, + tls_cert=certs['tls_cert'], + haproxy_versions=haproxy_versions, + client_ca_filename=client_ca_filename, + client_crl=crl_filename, + pool_tls_certs=pool_tls_certs) + self.clients[amphora.api_version].upload_config( + amphora, listener.id, config, + timeout_dict=timeout_dict) + self.clients[amphora.api_version].reload_listener( + amphora, listener.id, timeout_dict=timeout_dict) + + if has_tcp and not split_config: + # Generate HaProxy configuration from listener object + config = self.jinja_combo.build_config( + host_amphora=amphora, listeners=loadbalancer.listeners, + tls_cert=certs['tls_cert'], + haproxy_versions=haproxy_versions, + client_ca_filename=client_ca_filename, + client_crl=crl_filename, + pool_tls_certs=pool_tls_certs) + self.clients[amphora.api_version].upload_config( + amphora, loadbalancer.id, config, timeout_dict=timeout_dict) + self.clients[amphora.api_version].reload_listener( + amphora, loadbalancer.id, timeout_dict=timeout_dict) def _udp_update(self, listener, vip): LOG.debug("Amphora %s keepalivedlvs, updating " @@ -146,69 +210,132 @@ class HaproxyAmphoraLoadBalancerDriver( for amp in listener.load_balancer.amphorae: if amp.status != consts.DELETED: # Generate Keepalived LVS configuration from listener object + self._populate_amphora_api_version(amp) config = self.udp_jinja.build_config(listener=listener) - self.client.upload_udp_config(amp, listener.id, config) - self.client.reload_listener(amp, listener.id) + self.clients[amp.api_version].upload_udp_config( + amp, listener.id, config) + self.clients[amp.api_version].reload_listener( + amp, listener.id) - def update(self, listener, vip): - if listener.protocol == 'UDP': - self._udp_update(listener, vip) - else: - LOG.debug("Amphora %s haproxy, updating listener %s, " - "vip %s", self.__class__.__name__, - listener.protocol_port, - vip.ip_address) - - # Process listener certificate info - certs = self._process_tls_certificates(listener) - client_ca_filename = self._process_secret( - listener, listener.client_ca_tls_certificate_id) - crl_filename = self._process_secret( - listener, listener.client_crl_container_id) - pool_tls_certs = self._process_listener_pool_certs(listener) - - for amp in listener.load_balancer.amphorae: - if amp.status != consts.DELETED: - - haproxy_versions = self._get_haproxy_versions(amp) - - # Generate HaProxy configuration from listener object - config = self.jinja.build_config( - host_amphora=amp, listener=listener, - tls_cert=certs['tls_cert'], - haproxy_versions=haproxy_versions, - client_ca_filename=client_ca_filename, - client_crl=crl_filename, - pool_tls_certs=pool_tls_certs) - self.client.upload_config(amp, listener.id, config) - self.client.reload_listener(amp, listener.id) + def update(self, loadbalancer): + for amphora in loadbalancer.amphorae: + if amphora.status != consts.DELETED: + self.update_amphora_listeners(loadbalancer, amphora) def upload_cert_amp(self, amp, pem): LOG.debug("Amphora %s updating cert in REST driver " "with amphora id %s,", self.__class__.__name__, amp.id) - self.client.update_cert_for_rotation(amp, pem) + self._populate_amphora_api_version(amp) + self.clients[amp.api_version].update_cert_for_rotation(amp, pem) - def _apply(self, func, listener=None, amphora=None, *args): + def _apply(self, func_name, loadbalancer, amphora=None, *args): if amphora is None: - for amp in listener.load_balancer.amphorae: - if amp.status != consts.DELETED: - func(amp, listener.id, *args) + amphorae = loadbalancer.amphorae else: - if amphora.status != consts.DELETED: - func(amphora, listener.id, *args) + amphorae = [amphora] - def stop(self, listener, vip): - self._apply(self.client.stop_listener, listener) + for amp in amphorae: + if amp.status != consts.DELETED: + api_version = self._populate_amphora_api_version(amp) + # Check which config style to use + if api_version[0] == 0 and api_version[1] <= 5: + # 0.5 or earlier + LOG.warning( + 'Amphora %s for loadbalancer %s needs upgrade to ' + 'single process mode.', amp.id, loadbalancer.id) + for listener in loadbalancer.listeners: + getattr(self.clients[amp.api_version], func_name)( + amp, listener.id, *args) + else: + LOG.debug( + 'Amphora %s for loadbalancer %s is already in single ' + 'process mode.', amp.id, loadbalancer.id) + has_tcp = False + for listener in loadbalancer.listeners: + if listener.protocol == consts.PROTOCOL_UDP: + getattr(self.clients[amp.api_version], func_name)( + amp, listener.id, *args) + else: + has_tcp = True + if has_tcp: + getattr(self.clients[amp.api_version], func_name)( + amp, loadbalancer.id, *args) - def start(self, listener, vip, amphora=None): - self._apply(self.client.start_listener, listener, amphora) + def start(self, loadbalancer, amphora=None): + self._apply('start_listener', loadbalancer, amphora) - def delete(self, listener, vip): - self._apply(self.client.delete_listener, listener) + def delete(self, listener): + # Delete any UDP listeners the old way (we didn't update the way they + # are configured) + loadbalancer = listener.load_balancer + if listener.protocol == consts.PROTOCOL_UDP: + for amp in loadbalancer.amphorae: + if amp.status != consts.DELETED: + self._populate_amphora_api_version(amp) + self.clients[amp.api_version].delete_listener( + amp, listener.id) + return + + # In case the listener is not UDP, things get more complicated. + # We need to do this individually for each amphora in case some are + # using split config and others are using combined config. + for amp in loadbalancer.amphorae: + if amp.status != consts.DELETED: + api_version = self._populate_amphora_api_version(amp) + # Check which config style to use + if api_version[0] == 0 and api_version[1] <= 5: + # 0.5 or earlier + LOG.warning( + 'Amphora %s for loadbalancer %s needs upgrade to ' + 'single process mode.', amp.id, loadbalancer.id) + self.clients[amp.api_version].delete_listener( + amp, listener.id) + else: + LOG.debug( + 'Amphora %s for loadbalancer %s is already in single ' + 'process mode.', amp.id, loadbalancer.id) + self._combined_config_delete(amp, listener) + + def _combined_config_delete(self, amphora, listener): + # Remove the listener from the listener list on the LB before + # passing the whole thing over to update (so it'll actually delete) + listener.load_balancer.listeners.remove(listener) + + # Check if there's any certs that we need to delete + certs = self._process_tls_certificates(listener) + certs_to_delete = set() + if certs['tls_cert']: + certs_to_delete.add(certs['tls_cert'].id) + for sni_cert in certs['sni_certs']: + certs_to_delete.add(sni_cert.id) + + # Delete them (they'll be recreated before the reload if they are + # needed for other listeners anyway) + self._populate_amphora_api_version(amphora) + for cert_id in certs_to_delete: + self.clients[amphora.api_version].delete_cert_pem( + amphora, listener.load_balancer.id, + '{id}.pem'.format(id=cert_id)) + + # See how many non-UDP listeners we have left + non_udp_listener_count = len([ + 1 for l in listener.load_balancer.listeners + if l.protocol != consts.PROTOCOL_UDP]) + if non_udp_listener_count > 0: + # We have other listeners, so just update is fine. + # TODO(rm_work): This is a little inefficient since this duplicates + # a lot of the detection logic that has already been done, but it + # is probably safer to re-use the existing code-path. + self.update_amphora_listeners(listener.load_balancer, amphora) + else: + # Deleting the last listener, so really do the delete + self.clients[amphora.api_version].delete_listener( + amphora, listener.load_balancer.id) def get_info(self, amphora): - return self.client.get_info(amphora) + self._populate_amphora_api_version(amphora) + return self.clients[amphora.api_version].get_info(amphora) def get_diagnostics(self, amphora): pass @@ -218,6 +345,7 @@ class HaproxyAmphoraLoadBalancerDriver( def post_vip_plug(self, amphora, load_balancer, amphorae_network_config): if amphora.status != consts.DELETED: + self._populate_amphora_api_version(amphora) subnet = amphorae_network_config.get(amphora.id).vip_subnet # NOTE(blogan): using the vrrp port here because that # is what the allowed address pairs network driver sets @@ -242,9 +370,8 @@ class HaproxyAmphoraLoadBalancerDriver( 'mtu': port.network.mtu, 'host_routes': host_routes} try: - self.client.plug_vip(amphora, - load_balancer.vip.ip_address, - net_info) + self.clients[amphora.api_version].plug_vip( + amphora, load_balancer.vip.ip_address, net_info) except exc.Conflict: LOG.warning('VIP with MAC %(mac)s already exists on amphora, ' 'skipping post_vip_plug', @@ -264,13 +391,14 @@ class HaproxyAmphoraLoadBalancerDriver( 'fixed_ips': fixed_ips, 'mtu': port.network.mtu} try: - self.client.plug_network(amphora, port_info) + self._populate_amphora_api_version(amphora) + self.clients[amphora.api_version].plug_network(amphora, port_info) except exc.Conflict: LOG.warning('Network with MAC %(mac)s already exists on amphora, ' 'skipping post_network_plug', {'mac': port.mac_address}) - def _process_tls_certificates(self, listener): + def _process_tls_certificates(self, listener, amphora=None, obj_id=None): """Processes TLS data from the listener. Converts and uploads PEM data to the Amphora API @@ -290,15 +418,15 @@ class HaproxyAmphoraLoadBalancerDriver( sni_certs = data['sni_certs'] certs.extend(sni_certs) - for cert in certs: - pem = cert_parser.build_pem(cert) - md5 = hashlib.md5(pem).hexdigest() # nosec - name = '{id}.pem'.format(id=cert.id) - self._apply(self._upload_cert, listener, None, pem, md5, name) - + if amphora and obj_id: + for cert in certs: + pem = cert_parser.build_pem(cert) + md5 = hashlib.md5(pem).hexdigest() # nosec + name = '{id}.pem'.format(id=cert.id) + self._upload_cert(amphora, obj_id, pem, md5, name) return {'tls_cert': tls_cert, 'sni_certs': sni_certs} - def _process_secret(self, listener, secret_ref): + def _process_secret(self, listener, secret_ref, amphora=None, obj_id=None): """Get the secret from the cert manager and upload it to the amp. :returns: The filename of the secret in the amp. @@ -314,10 +442,14 @@ class HaproxyAmphoraLoadBalancerDriver( md5 = hashlib.md5(secret).hexdigest() # nosec id = hashlib.sha1(secret).hexdigest() # nosec name = '{id}.pem'.format(id=id) - self._apply(self._upload_cert, listener, None, secret, md5, name) + + if amphora and obj_id: + self._upload_cert( + amphora, obj_id, pem=secret, md5=md5, name=name) return name - def _process_listener_pool_certs(self, listener): + def _process_listener_pool_certs(self, listener, amphora=None, + obj_id=None): # {'POOL-ID': { # 'client_cert': client_full_filename, # 'ca_cert': ca_cert_full_filename, @@ -325,19 +457,20 @@ class HaproxyAmphoraLoadBalancerDriver( pool_certs_dict = dict() for pool in listener.pools: if pool.id not in pool_certs_dict: - pool_certs_dict[pool.id] = self._process_pool_certs(listener, - pool) + pool_certs_dict[pool.id] = self._process_pool_certs( + listener, pool, amphora, obj_id) for l7policy in listener.l7policies: if (l7policy.redirect_pool and l7policy.redirect_pool.id not in pool_certs_dict): pool_certs_dict[l7policy.redirect_pool.id] = ( - self._process_pool_certs(listener, l7policy.redirect_pool)) + self._process_pool_certs(listener, l7policy.redirect_pool, + amphora, obj_id)) return pool_certs_dict - def _process_pool_certs(self, listener, pool): + def _process_pool_certs(self, listener, pool, amphora=None, obj_id=None): pool_cert_dict = dict() - # Handle the cleint cert(s) and key + # Handle the client cert(s) and key if pool.tls_certificate_id: data = cert_parser.load_certificates_data(self.cert_manager, pool) pem = cert_parser.build_pem(data) @@ -347,15 +480,18 @@ class HaproxyAmphoraLoadBalancerDriver( pass md5 = hashlib.md5(pem).hexdigest() # nosec name = '{id}.pem'.format(id=data.id) - self._apply(self._upload_cert, listener, None, pem, md5, name) + if amphora and obj_id: + self._upload_cert(amphora, obj_id, pem=pem, md5=md5, name=name) pool_cert_dict['client_cert'] = os.path.join( CONF.haproxy_amphora.base_cert_dir, listener.id, name) if pool.ca_tls_certificate_id: - name = self._process_secret(listener, pool.ca_tls_certificate_id) + name = self._process_secret(listener, pool.ca_tls_certificate_id, + amphora, obj_id) pool_cert_dict['ca_cert'] = os.path.join( CONF.haproxy_amphora.base_cert_dir, listener.id, name) if pool.crl_container_id: - name = self._process_secret(listener, pool.crl_container_id) + name = self._process_secret(listener, pool.crl_container_id, + amphora, obj_id) pool_cert_dict['crl'] = os.path.join( CONF.haproxy_amphora.base_cert_dir, listener.id, name) @@ -363,13 +499,14 @@ class HaproxyAmphoraLoadBalancerDriver( def _upload_cert(self, amp, listener_id, pem, md5, name): try: - if self.client.get_cert_md5sum( + if self.clients[amp.api_version].get_cert_md5sum( amp, listener_id, name, ignore=(404,)) == md5: return except exc.NotFound: pass - self.client.upload_cert_pem(amp, listener_id, name, pem) + self.clients[amp.api_version].upload_cert_pem( + amp, listener_id, name, pem) def update_amphora_agent_config(self, amphora, agent_config, timeout_dict=None): @@ -389,8 +526,9 @@ class HaproxyAmphoraLoadBalancerDriver( new values. """ try: - self.client.update_agent_config(amphora, agent_config, - timeout_dict=timeout_dict) + self._populate_amphora_api_version(amphora) + self.clients[amphora.api_version].update_agent_config( + amphora, agent_config, timeout_dict=timeout_dict) except exc.NotFound: LOG.debug('Amphora {} does not support the update_agent_config ' 'API.'.format(amphora.id)) @@ -411,10 +549,9 @@ class CustomHostNameCheckingAdapter(requests.adapters.HTTPAdapter): self).init_poolmanager(*pool_args, **pool_kwargs) -class AmphoraAPIClient(object): +class AmphoraAPIClientBase(object): def __init__(self): - super(AmphoraAPIClient, self).__init__() - self.secure = False + super(AmphoraAPIClientBase, self).__init__() self.get = functools.partial(self.request, 'get') self.post = functools.partial(self.request, 'post') @@ -422,38 +559,29 @@ class AmphoraAPIClient(object): self.delete = functools.partial(self.request, 'delete') self.head = functools.partial(self.request, 'head') - self.start_listener = functools.partial(self._action, - consts.AMP_ACTION_START) - self.stop_listener = functools.partial(self._action, - consts.AMP_ACTION_STOP) - self.reload_listener = functools.partial(self._action, - consts.AMP_ACTION_RELOAD) - - self.start_vrrp = functools.partial(self._vrrp_action, - consts.AMP_ACTION_START) - self.stop_vrrp = functools.partial(self._vrrp_action, - consts.AMP_ACTION_STOP) - self.reload_vrrp = functools.partial(self._vrrp_action, - consts.AMP_ACTION_RELOAD) - self.session = requests.Session() self.session.cert = CONF.haproxy_amphora.client_cert self.ssl_adapter = CustomHostNameCheckingAdapter() self.session.mount('https://', self.ssl_adapter) - def _base_url(self, ip): + def _base_url(self, ip, api_version=None): if utils.is_ipv6_lla(ip): ip = '[{ip}%{interface}]'.format( ip=ip, interface=CONF.haproxy_amphora.lb_network_interface) elif utils.is_ipv6(ip): ip = '[{ip}]'.format(ip=ip) - return "https://{ip}:{port}/{version}/".format( + if api_version: + return "https://{ip}:{port}/{version}/".format( + ip=ip, + port=CONF.haproxy_amphora.bind_port, + version=api_version) + return "https://{ip}:{port}/".format( ip=ip, - port=CONF.haproxy_amphora.bind_port, - version=API_VERSION) + port=CONF.haproxy_amphora.bind_port) - def request(self, method, amp, path='/', timeout_dict=None, **kwargs): + def request(self, method, amp, path='/', timeout_dict=None, + retry_404=True, **kwargs): cfg_ha_amp = CONF.haproxy_amphora if timeout_dict is None: timeout_dict = {} @@ -468,7 +596,7 @@ class AmphoraAPIClient(object): LOG.debug("request url %s", path) _request = getattr(self.session, method.lower()) - _url = self._base_url(amp.lb_network_ip) + path + _url = self._base_url(amp.lb_network_ip, amp.api_version) + path LOG.debug("request url %s", _url) reqargs = { 'verify': CONF.haproxy_amphora.server_ca, @@ -481,7 +609,7 @@ class AmphoraAPIClient(object): self.ssl_adapter.uuid = amp.id exception = None # Keep retrying - for a in six.moves.xrange(conn_max_retries): + for dummy in six.moves.xrange(conn_max_retries): try: with warnings.catch_warnings(): warnings.filterwarnings( @@ -497,6 +625,8 @@ class AmphoraAPIClient(object): # amphora is not yet up, in which case retry. # Otherwise return the response quickly. if r.status_code == 404: + if not retry_404: + raise exc.NotFound() LOG.debug('Got a 404 (content-type: %(content_type)s) -- ' 'connection data: %(content)s', {'content_type': content_type, @@ -524,6 +654,32 @@ class AmphoraAPIClient(object): 'exception': exception}) raise driver_except.TimeOutException() + def get_api_version(self, amp): + amp.api_version = None + r = self.get(amp, retry_404=False) + # Handle 404 special as we don't want to log an ERROR on 404 + exc.check_exception(r, (404,)) + if r.status_code == 404: + raise exc.NotFound() + return r.json() + + +class AmphoraAPIClient0_5(AmphoraAPIClientBase): + def __init__(self): + super(AmphoraAPIClient0_5, self).__init__() + + self.start_listener = functools.partial(self._action, + consts.AMP_ACTION_START) + self.reload_listener = functools.partial(self._action, + consts.AMP_ACTION_RELOAD) + + self.start_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_START) + self.stop_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_STOP) + self.reload_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_RELOAD) + def upload_config(self, amp, listener_id, config, timeout_dict=None): r = self.put( amp, @@ -532,14 +688,6 @@ class AmphoraAPIClient(object): data=config) return exc.check_exception(r) - def get_listener_status(self, amp, listener_id): - r = self.get( - amp, - 'listeners/{listener_id}'.format(listener_id=listener_id)) - if exc.check_exception(r): - return r.json() - return None - def _action(self, action, amp, listener_id, timeout_dict=None): r = self.put(amp, 'listeners/{listener_id}/{action}'.format( listener_id=listener_id, action=action), timeout_dict=timeout_dict) @@ -630,3 +778,133 @@ class AmphoraAPIClient(object): def update_agent_config(self, amp, agent_config, timeout_dict=None): r = self.put(amp, 'config', timeout_dict, data=agent_config) return exc.check_exception(r) + + +class AmphoraAPIClient1_0(AmphoraAPIClientBase): + def __init__(self): + super(AmphoraAPIClient1_0, self).__init__() + + self.start_listener = functools.partial(self._action, + consts.AMP_ACTION_START) + self.reload_listener = functools.partial(self._action, + consts.AMP_ACTION_RELOAD) + + self.start_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_START) + self.stop_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_STOP) + self.reload_vrrp = functools.partial(self._vrrp_action, + consts.AMP_ACTION_RELOAD) + + def upload_config(self, amp, loadbalancer_id, config, timeout_dict=None): + r = self.put( + amp, + 'loadbalancer/{amphora_id}/{loadbalancer_id}/haproxy'.format( + amphora_id=amp.id, loadbalancer_id=loadbalancer_id), + timeout_dict, data=config) + return exc.check_exception(r) + + def get_listener_status(self, amp, listener_id): + r = self.get( + amp, + 'listeners/{listener_id}'.format(listener_id=listener_id)) + if exc.check_exception(r): + return r.json() + return None + + def _action(self, action, amp, object_id, timeout_dict=None): + r = self.put( + amp, 'loadbalancer/{object_id}/{action}'.format( + object_id=object_id, action=action), + timeout_dict=timeout_dict) + return exc.check_exception(r) + + def upload_cert_pem(self, amp, loadbalancer_id, pem_filename, pem_file): + r = self.put( + amp, + 'loadbalancer/{loadbalancer_id}/certificates/{filename}'.format( + loadbalancer_id=loadbalancer_id, filename=pem_filename), + data=pem_file) + return exc.check_exception(r) + + def get_cert_md5sum(self, amp, loadbalancer_id, pem_filename, + ignore=tuple()): + r = self.get( + amp, + 'loadbalancer/{loadbalancer_id}/certificates/{filename}'.format( + loadbalancer_id=loadbalancer_id, filename=pem_filename)) + if exc.check_exception(r, ignore): + return r.json().get("md5sum") + return None + + def delete_cert_pem(self, amp, loadbalancer_id, pem_filename): + r = self.delete( + amp, + 'loadbalancer/{loadbalancer_id}/certificates/{filename}'.format( + loadbalancer_id=loadbalancer_id, filename=pem_filename)) + return exc.check_exception(r, (404,)) + + def update_cert_for_rotation(self, amp, pem_file): + r = self.put(amp, 'certificate', data=pem_file) + return exc.check_exception(r) + + def delete_listener(self, amp, object_id): + r = self.delete( + amp, 'listeners/{object_id}'.format(object_id=object_id)) + return exc.check_exception(r, (404,)) + + def get_info(self, amp): + r = self.get(amp, "info") + if exc.check_exception(r): + return r.json() + return None + + def get_details(self, amp): + r = self.get(amp, "details") + if exc.check_exception(r): + return r.json() + return None + + def get_all_listeners(self, amp): + r = self.get(amp, "listeners") + if exc.check_exception(r): + return r.json() + return None + + def plug_network(self, amp, port): + r = self.post(amp, 'plug/network', + json=port) + return exc.check_exception(r) + + def plug_vip(self, amp, vip, net_info): + r = self.post(amp, + 'plug/vip/{vip}'.format(vip=vip), + json=net_info) + return exc.check_exception(r) + + def upload_vrrp_config(self, amp, config): + r = self.put(amp, 'vrrp/upload', data=config) + return exc.check_exception(r) + + def _vrrp_action(self, action, amp): + r = self.put(amp, 'vrrp/{action}'.format(action=action)) + return exc.check_exception(r) + + def get_interface(self, amp, ip_addr, timeout_dict=None): + r = self.get(amp, 'interface/{ip_addr}'.format(ip_addr=ip_addr), + timeout_dict=timeout_dict) + if exc.check_exception(r): + return r.json() + return None + + def upload_udp_config(self, amp, listener_id, config, timeout_dict=None): + r = self.put( + amp, + 'listeners/{amphora_id}/{listener_id}/udp_listener'.format( + amphora_id=amp.id, listener_id=listener_id), timeout_dict, + data=config) + return exc.check_exception(r) + + def update_agent_config(self, amp, agent_config, timeout_dict=None): + r = self.put(amp, 'config', timeout_dict, data=agent_config) + return exc.check_exception(r) diff --git a/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py b/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py index 42794a70d3..8de451a6da 100644 --- a/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py +++ b/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py @@ -45,13 +45,14 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): lambda amp: amp.status == constants.AMPHORA_ALLOCATED, loadbalancer.amphorae): + self._populate_amphora_api_version(amp) # Get the VIP subnet prefix for the amphora vip_cidr = amphorae_network_config[amp.id].vip_subnet.cidr # Generate Keepalived configuration from loadbalancer object config = templater.build_keepalived_config( loadbalancer, amp, vip_cidr) - self.client.upload_vrrp_config(amp, config) + self.clients[amp.api_version].upload_vrrp_config(amp, config) def stop_vrrp_service(self, loadbalancer): """Stop the vrrp services running on the loadbalancer's amphorae @@ -65,7 +66,8 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): lambda amp: amp.status == constants.AMPHORA_ALLOCATED, loadbalancer.amphorae): - self.client.stop_vrrp(amp) + self._populate_amphora_api_version(amp) + self.clients[amp.api_version].stop_vrrp(amp) def start_vrrp_service(self, loadbalancer): """Start the VRRP services of all amphorae of the loadbalancer @@ -80,7 +82,8 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): loadbalancer.amphorae): LOG.debug("Start VRRP Service on amphora %s .", amp.lb_network_ip) - self.client.start_vrrp(amp) + self._populate_amphora_api_version(amp) + self.clients[amp.api_version].start_vrrp(amp) def reload_vrrp_service(self, loadbalancer): """Reload the VRRP services of all amphorae of the loadbalancer @@ -94,8 +97,10 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): lambda amp: amp.status == constants.AMPHORA_ALLOCATED, loadbalancer.amphorae): - self.client.reload_vrrp(amp) + self._populate_amphora_api_version(amp) + self.clients[amp.api_version].reload_vrrp(amp) def get_vrrp_interface(self, amphora, timeout_dict=None): - return self.client.get_interface( + self._populate_amphora_api_version(amphora) + return self.clients[amphora.api_version].get_interface( amphora, amphora.vrrp_ip, timeout_dict=timeout_dict)['interface'] diff --git a/octavia/amphorae/drivers/noop_driver/driver.py b/octavia/amphorae/drivers/noop_driver/driver.py index 5138d5d84c..688f145afa 100644 --- a/octavia/amphorae/drivers/noop_driver/driver.py +++ b/octavia/amphorae/drivers/noop_driver/driver.py @@ -37,44 +37,41 @@ class NoopManager(object): super(NoopManager, self).__init__() self.amphoraconfig = {} - def update_amphora_listeners(self, listeners, amphora_index, - amphorae, timeout_dict): - amphora_id = amphorae[amphora_index].id - for listener in listeners: + def update_amphora_listeners(self, loadbalancer, amphora, timeout_dict): + amphora_id = amphora.id + for listener in loadbalancer.listeners: LOG.debug("Amphora noop driver update_amphora_listeners, " "listener %s, amphora %s, timeouts %s", listener.id, amphora_id, timeout_dict) self.amphoraconfig[(listener.id, amphora_id)] = ( listener, amphora_id, timeout_dict, "update_amp") - def update(self, listener, vip): + def update(self, loadbalancer): LOG.debug("Amphora %s no-op, update listener %s, vip %s", - self.__class__.__name__, listener.protocol_port, - vip.ip_address) - self.amphoraconfig[(listener.protocol_port, - vip.ip_address)] = (listener, vip, 'active') - - def stop(self, listener, vip): - LOG.debug("Amphora %s no-op, stop listener %s, vip %s", self.__class__.__name__, - listener.protocol_port, vip.ip_address) - self.amphoraconfig[(listener.protocol_port, - vip.ip_address)] = (listener, vip, 'stop') + tuple(l.protocol_port for l in loadbalancer.listeners), + loadbalancer.vip.ip_address) + self.amphoraconfig[ + (tuple(l.protocol_port for l in loadbalancer.listeners), + loadbalancer.vip.ip_address)] = (loadbalancer.listeners, + loadbalancer.vip, + 'active') - def start(self, listener, vip, amphora=None): - LOG.debug("Amphora %s no-op, start listener %s, vip %s, amp %s", - self.__class__.__name__, - listener.protocol_port, vip.ip_address, amphora) - self.amphoraconfig[(listener.protocol_port, - vip.ip_address, amphora)] = (listener, vip, - amphora, 'start') + def start(self, loadbalancer, amphora=None): + LOG.debug("Amphora %s no-op, start listeners, lb %s, amp %s", + self.__class__.__name__, loadbalancer.id, amphora) + self.amphoraconfig[ + (loadbalancer.id, amphora.id)] = (loadbalancer, amphora, + 'start') - def delete(self, listener, vip): + def delete(self, listener): LOG.debug("Amphora %s no-op, delete listener %s, vip %s", self.__class__.__name__, - listener.protocol_port, vip.ip_address) + listener.protocol_port, + listener.load_balancer.vip.ip_address) self.amphoraconfig[(listener.protocol_port, - vip.ip_address)] = (listener, vip, 'delete') + listener.load_balancer.vip.ip_address)] = ( + listener, listener.load_balancer.vip, 'delete') def get_info(self, amphora): LOG.debug("Amphora %s no-op, info amphora %s", @@ -124,27 +121,22 @@ class NoopAmphoraLoadBalancerDriver( super(NoopAmphoraLoadBalancerDriver, self).__init__() self.driver = NoopManager() - def update_amphora_listeners(self, listeners, amphora_index, - amphorae, timeout_dict): + def update_amphora_listeners(self, loadbalancer, amphora, timeout_dict): - self.driver.update_amphora_listeners(listeners, amphora_index, - amphorae, timeout_dict) + self.driver.update_amphora_listeners(loadbalancer, amphora, + timeout_dict) - def update(self, listener, vip): + def update(self, loadbalancer): - self.driver.update(listener, vip) + self.driver.update(loadbalancer) - def stop(self, listener, vip): + def start(self, loadbalancer, amphora=None): - self.driver.stop(listener, vip) + self.driver.start(loadbalancer, amphora) - def start(self, listener, vip, amphora=None): + def delete(self, listener): - self.driver.start(listener, vip, amphora) - - def delete(self, listener, vip): - - self.driver.delete(listener, vip) + self.driver.delete(listener) def get_info(self, amphora): diff --git a/octavia/cmd/health_manager.py b/octavia/cmd/health_manager.py index 547e205c30..ea0494f11f 100644 --- a/octavia/cmd/health_manager.py +++ b/octavia/cmd/health_manager.py @@ -122,3 +122,6 @@ def main(): process.join() except KeyboardInterrupt: process_cleanup() + +if __name__ == "__main__": + main() diff --git a/octavia/cmd/octavia_worker.py b/octavia/cmd/octavia_worker.py index 03ec52efd1..0655e6831a 100644 --- a/octavia/cmd/octavia_worker.py +++ b/octavia/cmd/octavia_worker.py @@ -39,3 +39,6 @@ def main(): workers=CONF.controller_worker.workers, args=(CONF,)) oslo_config_glue.setup(sm, CONF, reload_method="mutate") sm.run() + +if __name__ == "__main__": + main() diff --git a/octavia/common/jinja/haproxy/combined_listeners/__init__.py b/octavia/common/jinja/haproxy/combined_listeners/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py new file mode 100644 index 0000000000..fdf93b1408 --- /dev/null +++ b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py @@ -0,0 +1,499 @@ +# Copyright (c) 2015 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import re + +import jinja2 +import six + +from octavia.common.config import cfg +from octavia.common import constants +from octavia.common import utils as octavia_utils + +PROTOCOL_MAP = { + constants.PROTOCOL_TCP: 'tcp', + constants.PROTOCOL_HTTP: 'http', + constants.PROTOCOL_HTTPS: 'tcp', + constants.PROTOCOL_PROXY: 'proxy', + constants.PROTOCOL_TERMINATED_HTTPS: 'http' +} + +BALANCE_MAP = { + constants.LB_ALGORITHM_ROUND_ROBIN: 'roundrobin', + constants.LB_ALGORITHM_LEAST_CONNECTIONS: 'leastconn', + constants.LB_ALGORITHM_SOURCE_IP: 'source' +} + +CLIENT_AUTH_MAP = {constants.CLIENT_AUTH_NONE: 'none', + constants.CLIENT_AUTH_OPTIONAL: 'optional', + constants.CLIENT_AUTH_MANDATORY: 'required'} + +ACTIVE_PENDING_STATUSES = constants.SUPPORTED_PROVISIONING_STATUSES + ( + constants.DEGRADED,) + +BASE_PATH = '/var/lib/octavia' +BASE_CRT_DIR = BASE_PATH + '/certs' + +HAPROXY_TEMPLATE = os.path.abspath( + os.path.join(os.path.dirname(__file__), + 'templates/haproxy.cfg.j2')) + +CONF = cfg.CONF + +JINJA_ENV = None + + +class JinjaTemplater(object): + + def __init__(self, + base_amp_path=None, + base_crt_dir=None, + haproxy_template=None, + log_http=None, + log_server=None, + connection_logging=True): + """HaProxy configuration generation + + :param base_amp_path: Base path for amphora data + :param base_crt_dir: Base directory for certificate storage + :param haproxy_template: Absolute path to Jinja template + :param log_http: Haproxy HTTP logging path + :param log_server: Haproxy Server logging path + :param connection_logging: enable logging connections in haproxy + """ + + self.base_amp_path = base_amp_path or BASE_PATH + self.base_crt_dir = base_crt_dir or BASE_CRT_DIR + self.haproxy_template = haproxy_template or HAPROXY_TEMPLATE + self.log_http = log_http + self.log_server = log_server + self.connection_logging = connection_logging + + def build_config(self, host_amphora, listeners, tls_cert, + haproxy_versions, socket_path=None, + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): + """Convert a logical configuration to the HAProxy version + + :param host_amphora: The Amphora this configuration is hosted on + :param listener: The listener configuration + :param tls_cert: The TLS certificates for the listener + :param socket_path: The socket path for Haproxy process + :return: Rendered configuration + """ + + # Check for any backward compatibility items we need to check + # This is done here for upgrade scenarios where one amp in a + # pair might be running an older amphora version. + + feature_compatibility = {} + # Is it newer than haproxy 1.5? + if not (int(haproxy_versions[0]) < 2 and int(haproxy_versions[1]) < 6): + feature_compatibility[constants.HTTP_REUSE] = True + + return self.render_loadbalancer_obj( + host_amphora, listeners, tls_cert=tls_cert, + socket_path=socket_path, + feature_compatibility=feature_compatibility, + client_ca_filename=client_ca_filename, client_crl=client_crl, + pool_tls_certs=pool_tls_certs) + + def _get_template(self): + """Returns the specified Jinja configuration template.""" + global JINJA_ENV + if not JINJA_ENV: + template_loader = jinja2.FileSystemLoader( + searchpath=os.path.dirname(self.haproxy_template)) + JINJA_ENV = jinja2.Environment( + autoescape=True, + loader=template_loader, + trim_blocks=True, + lstrip_blocks=True) + JINJA_ENV.filters['hash_amp_id'] = octavia_utils.base64_sha1_string + return JINJA_ENV.get_template(os.path.basename(self.haproxy_template)) + + def _format_log_string(self, load_balancer, protocol): + log_format = CONF.haproxy_amphora.user_log_format.replace( + '{project_id}', load_balancer.project_id) + log_format = log_format.replace('{lb_id}', load_balancer.id) + + # Order of these filters matter. + # TODO(johnsom) Remove when HAProxy handles the format string + # with HTTP variables in TCP listeners. + # Currently it either throws an error or just fails + # to log the message. + if protocol not in constants.HAPROXY_HTTP_PROTOCOLS: + log_format = log_format.replace('%{+Q}r', '-') + log_format = log_format.replace('%r', '-') + log_format = log_format.replace('%{+Q}ST', '-') + log_format = log_format.replace('%ST', '-') + + log_format = log_format.replace(' ', '\ ') + return log_format + + def render_loadbalancer_obj(self, host_amphora, listeners, + tls_cert=None, socket_path=None, + feature_compatibility=None, + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): + """Renders a templated configuration from a load balancer object + + :param host_amphora: The Amphora this configuration is hosted on + :param listener: The listener configuration + :param tls_cert: The TLS certificates for the listener + :param client_ca_filename: The CA certificate for client authorization + :param socket_path: The socket path for Haproxy process + :return: Rendered configuration + """ + feature_compatibility = feature_compatibility or {} + loadbalancer = self._transform_loadbalancer( + host_amphora, + listeners[0].load_balancer, + listeners, + tls_cert, + feature_compatibility, + client_ca_filename=client_ca_filename, + client_crl=client_crl, + pool_tls_certs=pool_tls_certs) + if not socket_path: + socket_path = '%s/%s.sock' % (self.base_amp_path, + listeners[0].load_balancer.id) + return self._get_template().render( + {'loadbalancer': loadbalancer, + 'stats_sock': socket_path, + 'log_http': self.log_http, + 'log_server': self.log_server, + 'administrative_log_facility': + CONF.amphora_agent.administrative_log_facility, + 'user_log_facility': CONF.amphora_agent.user_log_facility, + 'connection_logging': self.connection_logging}, + constants=constants) + + def _transform_loadbalancer(self, host_amphora, loadbalancer, listeners, + tls_cert, feature_compatibility, + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): + """Transforms a load balancer into an object that will + + be processed by the templating system + """ + listener_transforms = [] + for listener in listeners: + if listener.protocol == constants.PROTOCOL_UDP: + continue + listener_transforms.append(self._transform_listener( + listener, tls_cert, feature_compatibility, loadbalancer, + client_ca_filename=client_ca_filename, client_crl=client_crl, + pool_tls_certs=pool_tls_certs)) + + ret_value = { + 'id': loadbalancer.id, + 'vip_address': loadbalancer.vip.ip_address, + 'listeners': listener_transforms, + 'topology': loadbalancer.topology, + 'enabled': loadbalancer.enabled, + 'peer_port': listeners[0].peer_port, + 'host_amphora': self._transform_amphora( + host_amphora, feature_compatibility), + 'amphorae': loadbalancer.amphorae + } + # NOTE(sbalukoff): Global connection limit should be a sum of all + # listeners' connection limits. + connection_limit_sum = 0 + for listener in listeners: + if listener.protocol == constants.PROTOCOL_UDP: + continue + if listener.connection_limit and listener.connection_limit > -1: + connection_limit_sum += listener.connection_limit + else: + # If *any* listener has no connection limit, global = MAX + connection_limit_sum = constants.HAPROXY_MAX_MAXCONN + # If there's a limit between 0 and MAX, set it, otherwise just set MAX + if 0 < connection_limit_sum < constants.HAPROXY_MAX_MAXCONN: + ret_value['global_connection_limit'] = connection_limit_sum + else: + ret_value['global_connection_limit'] = ( + constants.HAPROXY_MAX_MAXCONN) + return ret_value + + def _transform_amphora(self, amphora, feature_compatibility): + """Transform an amphora into an object that will + + be processed by the templating system. + """ + return { + 'id': amphora.id, + 'lb_network_ip': amphora.lb_network_ip, + 'vrrp_ip': amphora.vrrp_ip, + 'ha_ip': amphora.ha_ip, + 'vrrp_port_id': amphora.vrrp_port_id, + 'ha_port_id': amphora.ha_port_id, + 'role': amphora.role, + 'status': amphora.status, + 'vrrp_interface': amphora.vrrp_interface, + 'vrrp_priority': amphora.vrrp_priority + } + + def _transform_listener(self, listener, tls_cert, feature_compatibility, + loadbalancer, client_ca_filename=None, + client_crl=None, pool_tls_certs=None): + """Transforms a listener into an object that will + + be processed by the templating system + """ + ret_value = { + 'id': listener.id, + 'protocol_port': listener.protocol_port, + 'protocol_mode': PROTOCOL_MAP[listener.protocol], + 'protocol': listener.protocol, + 'insert_headers': listener.insert_headers, + 'enabled': listener.enabled, + 'user_log_format': self._format_log_string(loadbalancer, + listener.protocol), + 'timeout_client_data': ( + listener.timeout_client_data or + CONF.haproxy_amphora.timeout_client_data), + 'timeout_member_connect': ( + listener.timeout_member_connect or + CONF.haproxy_amphora.timeout_member_connect), + 'timeout_member_data': ( + listener.timeout_member_data or + CONF.haproxy_amphora.timeout_member_data), + 'timeout_tcp_inspect': (listener.timeout_tcp_inspect or + CONF.haproxy_amphora.timeout_tcp_inspect), + } + if listener.connection_limit and listener.connection_limit > -1: + ret_value['connection_limit'] = listener.connection_limit + else: + ret_value['connection_limit'] = constants.HAPROXY_MAX_MAXCONN + if listener.tls_certificate_id: + ret_value['default_tls_path'] = '%s.pem' % ( + os.path.join(self.base_crt_dir, + loadbalancer.id, + tls_cert.id)) + if listener.sni_containers: + ret_value['crt_dir'] = os.path.join( + self.base_crt_dir, loadbalancer.id) + if listener.client_ca_tls_certificate_id: + ret_value['client_ca_tls_path'] = '%s' % ( + os.path.join(self.base_crt_dir, loadbalancer.id, + client_ca_filename)) + ret_value['client_auth'] = CLIENT_AUTH_MAP.get( + listener.client_authentication) + if listener.client_crl_container_id: + ret_value['client_crl_path'] = '%s' % ( + os.path.join(self.base_crt_dir, loadbalancer.id, client_crl)) + + pools = [] + for x in listener.pools: + kwargs = {} + if pool_tls_certs and pool_tls_certs.get(x.id): + kwargs = {'pool_tls_certs': pool_tls_certs.get(x.id)} + pools.append(self._transform_pool( + x, feature_compatibility, **kwargs)) + ret_value['pools'] = pools + if listener.default_pool: + for pool in pools: + if pool['id'] == listener.default_pool.id: + ret_value['default_pool'] = pool + break + + l7policies = [self._transform_l7policy( + x, feature_compatibility, pool_tls_certs) + for x in listener.l7policies] + ret_value['l7policies'] = l7policies + return ret_value + + def _transform_pool(self, pool, feature_compatibility, + pool_tls_certs=None): + """Transforms a pool into an object that will + + be processed by the templating system + """ + ret_value = { + 'id': pool.id, + 'protocol': PROTOCOL_MAP[pool.protocol], + 'proxy_protocol': pool.protocol == constants.PROTOCOL_PROXY, + 'lb_algorithm': BALANCE_MAP.get(pool.lb_algorithm, 'roundrobin'), + 'members': [], + 'health_monitor': '', + 'session_persistence': '', + 'enabled': pool.enabled, + 'operating_status': pool.operating_status, + 'stick_size': CONF.haproxy_amphora.haproxy_stick_size, + constants.HTTP_REUSE: feature_compatibility.get( + constants.HTTP_REUSE, False), + 'ca_tls_path': '', + 'crl_path': '', + 'tls_enabled': pool.tls_enabled + } + members = [self._transform_member(x, feature_compatibility) + for x in pool.members] + ret_value['members'] = members + if pool.health_monitor: + ret_value['health_monitor'] = self._transform_health_monitor( + pool.health_monitor, feature_compatibility) + if pool.session_persistence: + ret_value[ + 'session_persistence'] = self._transform_session_persistence( + pool.session_persistence, feature_compatibility) + if (pool.tls_certificate_id and pool_tls_certs and + pool_tls_certs.get('client_cert')): + ret_value['client_cert'] = pool_tls_certs.get('client_cert') + if (pool.ca_tls_certificate_id and pool_tls_certs and + pool_tls_certs.get('ca_cert')): + ret_value['ca_cert'] = pool_tls_certs.get('ca_cert') + if (pool.crl_container_id and pool_tls_certs and + pool_tls_certs.get('crl')): + ret_value['crl'] = pool_tls_certs.get('crl') + + return ret_value + + @staticmethod + def _transform_session_persistence(persistence, feature_compatibility): + """Transforms session persistence into an object that will + + be processed by the templating system + """ + return { + 'type': persistence.type, + 'cookie_name': persistence.cookie_name + } + + @staticmethod + def _transform_member(member, feature_compatibility): + """Transforms a member into an object that will + + be processed by the templating system + """ + return { + 'id': member.id, + 'address': member.ip_address, + 'protocol_port': member.protocol_port, + 'weight': member.weight, + 'enabled': member.enabled, + 'subnet_id': member.subnet_id, + 'operating_status': member.operating_status, + 'monitor_address': member.monitor_address, + 'monitor_port': member.monitor_port, + 'backup': member.backup + } + + def _transform_health_monitor(self, monitor, feature_compatibility): + """Transforms a health monitor into an object that will + + be processed by the templating system + """ + codes = None + if monitor.expected_codes: + codes = '|'.join(self._expand_expected_codes( + monitor.expected_codes)) + return { + 'id': monitor.id, + 'type': monitor.type, + 'delay': monitor.delay, + 'timeout': monitor.timeout, + 'fall_threshold': monitor.fall_threshold, + 'rise_threshold': monitor.rise_threshold, + 'http_method': monitor.http_method, + 'url_path': monitor.url_path, + 'expected_codes': codes, + 'enabled': monitor.enabled, + 'http_version': monitor.http_version, + 'domain_name': monitor.domain_name, + } + + def _transform_l7policy(self, l7policy, feature_compatibility, + pool_tls_certs=None): + """Transforms an L7 policy into an object that will + + be processed by the templating system + """ + ret_value = { + 'id': l7policy.id, + 'action': l7policy.action, + 'redirect_url': l7policy.redirect_url, + 'redirect_prefix': l7policy.redirect_prefix, + 'enabled': l7policy.enabled + } + if l7policy.redirect_pool: + kwargs = {} + if pool_tls_certs and pool_tls_certs.get( + l7policy.redirect_pool.id): + kwargs = {'pool_tls_certs': + pool_tls_certs.get(l7policy.redirect_pool.id)} + ret_value['redirect_pool'] = self._transform_pool( + l7policy.redirect_pool, feature_compatibility, **kwargs) + else: + ret_value['redirect_pool'] = None + if (l7policy.action in [constants.L7POLICY_ACTION_REDIRECT_TO_URL, + constants.L7POLICY_ACTION_REDIRECT_PREFIX] and + l7policy.redirect_http_code): + ret_value['redirect_http_code'] = l7policy.redirect_http_code + else: + ret_value['redirect_http_code'] = None + l7rules = [self._transform_l7rule(x, feature_compatibility) + for x in l7policy.l7rules if x.enabled] + ret_value['l7rules'] = l7rules + return ret_value + + def _transform_l7rule(self, l7rule, feature_compatibility): + """Transforms an L7 rule into an object that will + + be processed by the templating system + """ + return { + 'id': l7rule.id, + 'type': l7rule.type, + 'compare_type': l7rule.compare_type, + 'key': l7rule.key, + 'value': self._escape_haproxy_config_string(l7rule.value), + 'invert': l7rule.invert, + 'enabled': l7rule.enabled + } + + @staticmethod + def _escape_haproxy_config_string(value): + """Escapes certain characters in a given string such that + + haproxy will parse the string as a single value + """ + # Escape backslashes first + value = re.sub(r'\\', r'\\\\', value) + # Spaces next + value = re.sub(' ', '\\ ', value) + return value + + @staticmethod + def _expand_expected_codes(codes): + """Expand the expected code string in set of codes. + + 200-204 -> 200, 201, 202, 204 + 200, 203 -> 200, 203 + """ + + retval = set() + for code in codes.replace(',', ' ').split(' '): + code = code.strip() + + if not code: + continue + elif '-' in code: + low, hi = code.split('-')[:2] + retval.update( + str(i) for i in six.moves.xrange(int(low), int(hi) + 1)) + else: + retval.add(code) + return sorted(retval) diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/base.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/base.j2 new file mode 100644 index 0000000000..c06093ceb2 --- /dev/null +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/base.j2 @@ -0,0 +1,52 @@ +{# Copyright (c) 2015 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +#} +# Configuration for loadbalancer {{ loadbalancer_id }} +global + daemon + user nobody + log {{ log_http | default('/dev/log', true)}} local{{ user_log_facility }} + log {{ log_server | default('/dev/log', true)}} local{{ administrative_log_facility }} notice + stats socket {{ sock_path }} mode 0666 level user + {% if loadbalancer.global_connection_limit is defined %} + maxconn {{ loadbalancer.global_connection_limit }} + {% endif %} + {% set found_ns = namespace(found=false) %} + {% for listener in loadbalancer.listeners if listener.enabled %} + {% for pool in listener.pools if pool.enabled %} + {% if pool.health_monitor and pool.health_monitor.enabled and + pool.health_monitor.type == constants.HEALTH_MONITOR_PING and + found_ns.found == false %} + {% set found_ns.found = true %} + external-check + {% endif %} + {% endfor %} + {% endfor %} + +defaults + {% if connection_logging %} + log global + {% else %} + no log + {% endif %} + retries 3 + option redispatch + option splice-request + option splice-response + option http-keep-alive + +{% block peers %}{% endblock peers %} + +{% block proxies %}{% endblock proxies %} diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 new file mode 100644 index 0000000000..3df3dcecc0 --- /dev/null +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 @@ -0,0 +1,40 @@ +{# Copyright (c) 2015 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +#} +{% extends 'base.j2' %} + + +{% from 'macros.j2' import frontend_macro, backend_macro %} +{% from 'macros.j2' import peers_macro %} + + +{% set loadbalancer_id = loadbalancer.id %} +{% set sock_path = stats_sock %} + + +{% block peers %} +{{ peers_macro(constants, loadbalancer) }} +{% endblock peers %} + +{% block proxies %} + {% if loadbalancer.enabled %} + {% for listener in loadbalancer.listeners if listener.enabled %} + {{- frontend_macro(constants, listener, loadbalancer.vip_address) }} + {% for pool in listener.pools if pool.enabled %} + {{- backend_macro(constants, listener, pool, loadbalancer) }} + {% endfor %} + {% endfor %} + {% endif %} +{% endblock proxies %} diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 new file mode 100644 index 0000000000..65e2e455e1 --- /dev/null +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 @@ -0,0 +1,370 @@ +{# Copyright (c) 2015 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +#} +{% macro peers_macro(constants, loadbalancer) %} + {% if loadbalancer.topology == constants.TOPOLOGY_ACTIVE_STANDBY %} +peers {{ "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }} + {% for amp in loadbalancer.amphorae if ( + amp.status == constants.AMPHORA_ALLOCATED) %} + {# HAProxy has peer name limitations, thus the hash filter #} + peer {{ amp.id|hash_amp_id|replace('=', '') }} {{ + amp.vrrp_ip }}:{{ constants.HAPROXY_BASE_PEER_PORT }} + {% endfor %} + {% endif %} +{% endmacro %} + + +{% macro bind_macro(constants, listener, lb_vip_address) %} + {% if listener.default_tls_path %} + {% set def_crt_opt = ("ssl crt %s"|format( + listener.default_tls_path)|trim()) %} + {% else %} + {% set def_crt_opt = "" %} + {% endif %} + {% if listener.crt_dir %} + {% set crt_dir_opt = "crt %s"|format(listener.crt_dir)|trim() %} + {% else %} + {% set crt_dir_opt = "" %} + {% endif %} + {% if listener.client_ca_tls_path and listener.client_auth %} + {% set client_ca_opt = "ca-file %s verify %s"|format(listener.client_ca_tls_path, listener.client_auth)|trim() %} + {% else %} + {% set client_ca_opt = "" %} + {% endif %} + {% if listener.client_crl_path and listener.client_ca_tls_path %} + {% set ca_crl_opt = "crl-file %s"|format(listener.client_crl_path)|trim() %} + {% else %} + {% set ca_crl_opt = "" %} + {% endif %} +bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{ +"%s %s %s %s"|format(def_crt_opt, crt_dir_opt, client_ca_opt, ca_crl_opt)|trim() }} +{% endmacro %} + + +{% macro l7rule_compare_type_macro(constants, ctype) %} + {% if ctype == constants.L7RULE_COMPARE_TYPE_REGEX %} + {{- "-m reg" -}} + {% elif ctype == constants.L7RULE_COMPARE_TYPE_STARTS_WITH %} + {{- "-m beg" -}} + {% elif ctype == constants.L7RULE_COMPARE_TYPE_ENDS_WITH %} + {{- "-m end" -}} + {% elif ctype == constants.L7RULE_COMPARE_TYPE_CONTAINS %} + {{- "-m sub" -}} + {% elif ctype == constants.L7RULE_COMPARE_TYPE_EQUAL_TO %} + {{- "-m str" -}} + {% endif %} +{% endmacro %} + + +{% macro l7rule_macro(constants, l7rule) %} + {% if l7rule.type == constants.L7RULE_TYPE_HOST_NAME %} + acl {{ l7rule.id }} req.hdr(host) -i {{ l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_PATH %} + acl {{ l7rule.id }} path {{ l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_FILE_TYPE %} + acl {{ l7rule.id }} path_end {{ l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_HEADER %} + acl {{ l7rule.id }} req.hdr({{ l7rule.key }}) {{ + l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_COOKIE %} + acl {{ l7rule.id }} req.cook({{ l7rule.key }}) {{ + l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_CONN_HAS_CERT %} + acl {{ l7rule.id }} ssl_c_used + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_VERIFY_RESULT %} + acl {{ l7rule.id }} ssl_c_verify eq {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_DN_FIELD %} + acl {{ l7rule.id }} ssl_c_s_dn({{ l7rule.key }}) {{ + l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} + {% endif %} +{% endmacro %} + + +{% macro l7rule_invert_macro(invert) %} + {% if invert %} + {{- "!" -}} + {% endif %} +{% endmacro %} + + +{% macro l7rule_list_macro(l7policy) %} + {% for l7rule in l7policy.l7rules %} + {{- " " -}}{{- l7rule_invert_macro(l7rule.invert) -}}{{- l7rule.id -}} + {% endfor %} +{% endmacro %} + + +{% macro l7policy_macro(constants, l7policy, listener) %} + {% for l7rule in l7policy.l7rules %} + {{- l7rule_macro(constants, l7rule) -}} + {% endfor %} + {% if l7policy.redirect_http_code %} + {% set redirect_http_code_opt = " code %s"|format( + l7policy.redirect_http_code) %} + {% else %} + {% set redirect_http_code_opt = "" %} + {% endif %} + {% if l7policy.action == constants.L7POLICY_ACTION_REJECT %} + http-request deny if{{ l7rule_list_macro(l7policy) }} + {% elif l7policy.action == constants.L7POLICY_ACTION_REDIRECT_TO_URL %} + redirect {{- redirect_http_code_opt }} location {{ l7policy.redirect_url }} if{{ l7rule_list_macro(l7policy) }} + {% elif l7policy.action == constants.L7POLICY_ACTION_REDIRECT_TO_POOL and l7policy.redirect_pool.enabled %} + use_backend {{ l7policy.redirect_pool.id }}:{{ listener.id }} if{{ l7rule_list_macro(l7policy) }} + {% elif l7policy.action == constants.L7POLICY_ACTION_REDIRECT_PREFIX %} + redirect {{- redirect_http_code_opt }} prefix {{ l7policy.redirect_prefix }} if{{ l7rule_list_macro(l7policy) }} + {% endif %} +{% endmacro %} + + +{% macro frontend_macro(constants, listener, lb_vip_address) %} +frontend {{ listener.id }} + log-format {{ listener.user_log_format }} + {% if listener.connection_limit is defined %} + maxconn {{ listener.connection_limit }} + {% endif %} + {% if (listener.protocol.lower() == + constants.PROTOCOL_TERMINATED_HTTPS.lower()) %} + redirect scheme https if !{ ssl_fc } + {% endif %} + {{ bind_macro(constants, listener, lb_vip_address)|trim() }} + mode {{ listener.protocol_mode }} + {% for l7policy in listener.l7policies if (l7policy.enabled and + l7policy.l7rules|length > 0) %} + {{- l7policy_macro(constants, l7policy, listener) -}} + {% endfor %} + {% if listener.default_pool and listener.default_pool.enabled %} + default_backend {{ listener.default_pool.id }}:{{ listener.id }} + {% endif %} + timeout client {{ listener.timeout_client_data }} + {% if listener.timeout_tcp_inspect %} + tcp-request inspect-delay {{ listener.timeout_tcp_inspect }} + {% endif %} +{% endmacro %} + + +{% macro member_macro(constants, pool, member) %} + {% if pool.health_monitor and pool.health_monitor.enabled %} + {% if member.monitor_address %} + {% set monitor_addr_opt = " addr %s"|format(member.monitor_address) %} + {% else %} + {% set monitor_addr_opt = "" %} + {% endif %} + {% if member.monitor_port %} + {% set monitor_port_opt = " port %s"|format(member.monitor_port) %} + {% else %} + {% set monitor_port_opt = "" %} + {% endif %} + {% if pool.health_monitor.type == constants.HEALTH_MONITOR_HTTPS %} + {% set monitor_ssl_opt = " check-ssl verify none" %} + {% else %} + {% set monitor_ssl_opt = "" %} + {% endif %} + {% set hm_opt = " check%s inter %ds fall %d rise %d%s%s"|format( + monitor_ssl_opt, pool.health_monitor.delay, + pool.health_monitor.fall_threshold, + pool.health_monitor.rise_threshold, monitor_addr_opt, + monitor_port_opt) %} + {% else %} + {% set hm_opt = "" %} + {% endif %} + {% if (pool.session_persistence.type == + constants.SESSION_PERSISTENCE_HTTP_COOKIE) %} + {% set persistence_opt = " cookie %s"|format(member.id) %} + {% else %} + {% set persistence_opt = "" %} + {% endif %} + {% if pool.proxy_protocol %} + {% set proxy_protocol_opt = " send-proxy" %} + {% else %} + {% set proxy_protocol_opt = "" %} + {% endif %} + {% if member.backup %} + {% set member_backup_opt = " backup" %} + {% else %} + {% set member_backup_opt = "" %} + {% endif %} + {% if member.enabled %} + {% set member_enabled_opt = "" %} + {% else %} + {% set member_enabled_opt = " disabled" %} + {% endif %} + {% if pool.tls_enabled %} + {% set def_opt_prefix = " ssl" %} + {% set def_sni_opt = " sni ssl_fc_sni" %} + {% else %} + {% set def_opt_prefix = "" %} + {% set def_sni_opt = "" %} + {% endif %} + {% if pool.client_cert and pool.tls_enabled %} + {% set def_crt_opt = " crt %s"|format(pool.client_cert) %} + {% else %} + {% set def_crt_opt = "" %} + {% endif %} + {% if pool.ca_cert and pool.tls_enabled %} + {% set ca_opt = " ca-file %s"|format(pool.ca_cert) %} + {% set def_verify_opt = " verify required" %} + {% if pool.crl %} + {% set crl_opt = " crl-file %s"|format(pool.crl) %} + {% else %} + {% set def_verify_opt = "" %} + {% endif %} + {% elif pool.tls_enabled %} + {% set def_verify_opt = " verify none" %} + {% endif %} + {{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s%s%s%s"|e|format( + member.id, member.address, member.protocol_port, member.weight, + hm_opt, persistence_opt, proxy_protocol_opt, member_backup_opt, + member_enabled_opt, def_opt_prefix, def_crt_opt, ca_opt, crl_opt, + def_verify_opt, def_sni_opt)|trim() }} +{% endmacro %} + + +{% macro backend_macro(constants, listener, pool, loadbalancer) %} +backend {{ pool.id }}:{{ listener.id }} + {% if pool.protocol.lower() == constants.PROTOCOL_PROXY.lower() %} + mode {{ listener.protocol_mode }} + {% else %} + mode {{ pool.protocol }} + {% endif %} + {% if pool.get(constants.HTTP_REUSE, False) and ( + pool.protocol.lower() == constants.PROTOCOL_HTTP.lower() or + (pool.protocol.lower() == constants.PROTOCOL_PROXY.lower() and + listener.protocol_mode.lower() == + constants.PROTOCOL_HTTP.lower()))%} + http-reuse safe + {% endif %} + balance {{ pool.lb_algorithm }} + {% if pool.session_persistence %} + {% if (pool.session_persistence.type == + constants.SESSION_PERSISTENCE_SOURCE_IP) %} + {% if loadbalancer.topology == constants.TOPOLOGY_ACTIVE_STANDBY %} + stick-table type ip size {{ pool.stick_size }} peers {{ + "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }} + {% else %} + stick-table type ip size {{ pool.stick_size }} + {% endif %} + stick on src + {% elif (pool.session_persistence.type == + constants.SESSION_PERSISTENCE_APP_COOKIE) %} + {% if loadbalancer.topology == constants.TOPOLOGY_ACTIVE_STANDBY %} + stick-table type string len 64 size {{ + pool.stick_size }} peers {{ + "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }} + {% else %} + stick-table type string len 64 size {{ pool.stick_size }} + {% endif %} + stick store-response res.cook({{ pool.session_persistence.cookie_name }}) + stick match req.cook({{ pool.session_persistence.cookie_name }}) + {% elif (pool.session_persistence.type == + constants.SESSION_PERSISTENCE_HTTP_COOKIE) %} + cookie SRV insert indirect nocache + {% endif %} + {% endif %} + {% if pool.health_monitor and pool.health_monitor.enabled %} + timeout check {{ pool.health_monitor.timeout }}s + {% if (pool.health_monitor.type == + constants.HEALTH_MONITOR_HTTP or pool.health_monitor.type == + constants.HEALTH_MONITOR_HTTPS) %} + {% if (pool.health_monitor.http_version and + pool.health_monitor.http_version == 1.1 and + pool.health_monitor.domain_name) %} + option httpchk {{ pool.health_monitor.http_method }} {{ pool.health_monitor.url_path }} HTTP/ + {{- pool.health_monitor.http_version -}}{{- "\\r\\n" | safe -}} + Host:\ {{ pool.health_monitor.domain_name }} + {% elif pool.health_monitor.http_version %} + option httpchk {{ pool.health_monitor.http_method }} {{ pool.health_monitor.url_path }} HTTP/ + {{- pool.health_monitor.http_version -}}{{- "\\r\\n" | safe }} + {% else %} + option httpchk {{ pool.health_monitor.http_method }} {{ pool.health_monitor.url_path }} + {% endif %} + http-check expect rstatus {{ pool.health_monitor.expected_codes }} + {% endif %} + {% if pool.health_monitor.type == constants.HEALTH_MONITOR_TLS_HELLO %} + option ssl-hello-chk + {% endif %} + {% if pool.health_monitor.type == constants.HEALTH_MONITOR_PING %} + option external-check + external-check command /var/lib/octavia/ping-wrapper.sh + {% endif %} + {% endif %} + {% if pool.protocol.lower() == constants.PROTOCOL_HTTP.lower() %} + {% if listener.insert_headers.get('X-Forwarded-For', + 'False').lower() == 'true' %} + option forwardfor + {% endif %} + {% if listener.insert_headers.get('X-Forwarded-Port', + 'False').lower() == 'true' %} + http-request set-header X-Forwarded-Port %[dst_port] + {% endif %} + {% endif %} + {% if listener.insert_headers.get('X-Forwarded-Proto', + 'False').lower() == 'true' %} + {% if listener.protocol.lower() == constants.PROTOCOL_HTTP.lower() %} + http-request set-header X-Forwarded-Proto http + {% elif listener.protocol.lower() == + constants.PROTOCOL_TERMINATED_HTTPS.lower() %} + http-request set-header X-Forwarded-Proto https + {% endif %} + {% endif %} + {% if listener.protocol.lower() == constants.PROTOCOL_TERMINATED_HTTPS.lower() %} + {% if listener.insert_headers.get('X-SSL-Client-Verify', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-Verify %[ssl_c_verify] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-Has-Cert', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-Has-Cert %[ssl_c_used] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-DN', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-CN', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Issuer', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-SHA1', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-Not-Before', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-Not-Before %{+Q}[ssl_c_notbefore] + {% endif %} + {% if listener.insert_headers.get('X-SSL-Client-Not-After', + 'False').lower() == 'true' %} + http-request set-header X-SSL-Client-Not-After %{+Q}[ssl_c_notafter] + {% endif %} + {% endif %} + {% if listener.connection_limit is defined %} + fullconn {{ listener.connection_limit }} + {% endif %} + option allbackups + timeout connect {{ listener.timeout_member_connect }} + timeout server {{ listener.timeout_member_data }} + {% for member in pool.members %} + {{- member_macro(constants, pool, member) -}} + {% endfor %} +{% endmacro %} diff --git a/octavia/common/jinja/haproxy/split_listeners/__init__.py b/octavia/common/jinja/haproxy/split_listeners/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/common/jinja/haproxy/jinja_cfg.py b/octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py similarity index 100% rename from octavia/common/jinja/haproxy/jinja_cfg.py rename to octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py diff --git a/octavia/common/jinja/haproxy/templates/base.j2 b/octavia/common/jinja/haproxy/split_listeners/templates/base.j2 similarity index 100% rename from octavia/common/jinja/haproxy/templates/base.j2 rename to octavia/common/jinja/haproxy/split_listeners/templates/base.j2 diff --git a/octavia/common/jinja/haproxy/templates/haproxy.cfg.j2 b/octavia/common/jinja/haproxy/split_listeners/templates/haproxy.cfg.j2 similarity index 100% rename from octavia/common/jinja/haproxy/templates/haproxy.cfg.j2 rename to octavia/common/jinja/haproxy/split_listeners/templates/haproxy.cfg.j2 diff --git a/octavia/common/jinja/haproxy/templates/macros.j2 b/octavia/common/jinja/haproxy/split_listeners/templates/macros.j2 similarity index 100% rename from octavia/common/jinja/haproxy/templates/macros.j2 rename to octavia/common/jinja/haproxy/split_listeners/templates/macros.j2 diff --git a/octavia/controller/healthmanager/health_drivers/update_db.py b/octavia/controller/healthmanager/health_drivers/update_db.py index 2ccfdddd66..356e366365 100644 --- a/octavia/controller/healthmanager/health_drivers/update_db.py +++ b/octavia/controller/healthmanager/health_drivers/update_db.py @@ -110,7 +110,7 @@ class UpdateHealthDb(update_base.HealthUpdateBase): :type map: string :returns: null - The input health data structure is shown as below:: + The input v1 health data structure is shown as below:: health = { "id": self.FAKE_UUID_1, @@ -125,6 +125,33 @@ class UpdateHealthDb(update_base.HealthUpdateBase): } } + Example V2 message:: + + {"id": "", + "seq": 67, + "listeners": { + "": { + "status": "OPEN", + "stats": { + "tx": 0, + "rx": 0, + "conns": 0, + "totconns": 0, + "ereq": 0 + } + } + }, + "pools": { + ":": { + "status": "UP", + "members": { + "": "no check" + } + } + }, + "ver": 2 + } + """ session = db_api.get_session() @@ -134,10 +161,15 @@ class UpdateHealthDb(update_base.HealthUpdateBase): ignore_listener_count = False if db_lb: - expected_listener_count = len(db_lb.get('listeners', {})) + expected_listener_count = 0 if 'PENDING' in db_lb['provisioning_status']: ignore_listener_count = True else: + for key, listener in db_lb.get('listeners', {}).items(): + # disabled listeners don't report from the amphora + if listener['enabled']: + expected_listener_count += 1 + # If this is a heartbeat older than versioning, handle # UDP special for backward compatibility. if 'ver' not in health: @@ -231,6 +263,8 @@ class UpdateHealthDb(update_base.HealthUpdateBase): else: lb_status = constants.ONLINE + health_msg_version = health.get('ver', 0) + for listener_id in db_lb.get('listeners', {}): db_op_status = db_lb['listeners'][listener_id]['operating_status'] listener_status = None @@ -267,11 +301,36 @@ class UpdateHealthDb(update_base.HealthUpdateBase): if not listener: continue - pools = listener['pools'] + if health_msg_version < 2: + raw_pools = listener['pools'] + + # normalize the pool IDs. Single process listener pools + # have the listener id appended with an ':' seperator. + # Old multi-process listener pools only have a pool ID. + # This makes sure the keys are only pool IDs. + pools = {(k + ' ')[:k.rfind(':')]: v for k, v in + raw_pools.items()} + + for db_pool_id in db_lb.get('pools', {}): + # If we saw this pool already on another listener, skip it. + if db_pool_id in processed_pools: + continue + db_pool_dict = db_lb['pools'][db_pool_id] + lb_status = self._process_pool_status( + session, db_pool_id, db_pool_dict, pools, + lb_status, processed_pools, potential_offline_pools) + + if health_msg_version >= 2: + raw_pools = health['pools'] + + # normalize the pool IDs. Single process listener pools + # have the listener id appended with an ':' seperator. + # Old multi-process listener pools only have a pool ID. + # This makes sure the keys are only pool IDs. + pools = {(k + ' ')[:k.rfind(':')]: v for k, v in raw_pools.items()} for db_pool_id in db_lb.get('pools', {}): - # If we saw this pool already on another listener - # skip it. + # If we saw this pool already, skip it. if db_pool_id in processed_pools: continue db_pool_dict = db_lb['pools'][db_pool_id] @@ -416,7 +475,7 @@ class UpdateStatsDb(update_base.StatsUpdateBase, stats.StatsMixin): :type map: string :returns: null - Example:: + Example V1 message:: health = { "id": self.FAKE_UUID_1, @@ -440,6 +499,33 @@ class UpdateStatsDb(update_base.StatsUpdateBase, stats.StatsMixin): } } + Example V2 message:: + + {"id": "", + "seq": 67, + "listeners": { + "": { + "status": "OPEN", + "stats": { + "tx": 0, + "rx": 0, + "conns": 0, + "totconns": 0, + "ereq": 0 + } + } + }, + "pools": { + ":": { + "status": "UP", + "members": { + "": "no check" + } + } + }, + "ver": 2 + } + """ session = db_api.get_session() diff --git a/octavia/controller/worker/v1/controller_worker.py b/octavia/controller/worker/v1/controller_worker.py index 9114344854..fa157f9eb3 100644 --- a/octavia/controller/worker/v1/controller_worker.py +++ b/octavia/controller/worker/v1/controller_worker.py @@ -235,13 +235,14 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine): raise db_exceptions.NoResultFound load_balancer = listener.load_balancer + listeners = load_balancer.listeners create_listener_tf = self._taskflow_load(self._listener_flows. get_create_listener_flow(), store={constants.LOADBALANCER: load_balancer, constants.LISTENERS: - [listener]}) + listeners}) with tf_logging.DynamicLoggingListener(create_listener_tf, log=LOG): create_listener_tf.run() diff --git a/octavia/controller/worker/v1/flows/amphora_flows.py b/octavia/controller/worker/v1/flows/amphora_flows.py index 966de2a54b..22514ddeae 100644 --- a/octavia/controller/worker/v1/flows/amphora_flows.py +++ b/octavia/controller/worker/v1/flows/amphora_flows.py @@ -470,7 +470,7 @@ class AmphoraFlows(object): update_amps_subflow.add( amphora_driver_tasks.AmpListenersUpdate( name=constants.AMP_LISTENER_UPDATE + '-' + str(amp_index), - requires=(constants.LISTENERS, constants.AMPHORAE), + requires=(constants.LOADBALANCER, constants.AMPHORAE), inject={constants.AMPHORA_INDEX: amp_index, constants.TIMEOUT_DICT: timeout_dict})) amp_index += 1 @@ -514,8 +514,7 @@ class AmphoraFlows(object): requires=constants.AMPHORA)) failover_amphora_flow.add(amphora_driver_tasks.ListenersStart( - requires=(constants.LOADBALANCER, constants.LISTENERS, - constants.AMPHORA))) + requires=(constants.LOADBALANCER, constants.AMPHORA))) failover_amphora_flow.add( database_tasks.DisableAmphoraHealthMonitoring( rebind={constants.AMPHORA: constants.FAILED_AMPHORA}, diff --git a/octavia/controller/worker/v1/flows/health_monitor_flows.py b/octavia/controller/worker/v1/flows/health_monitor_flows.py index 81a37741fa..e1d537b6a2 100644 --- a/octavia/controller/worker/v1/flows/health_monitor_flows.py +++ b/octavia/controller/worker/v1/flows/health_monitor_flows.py @@ -37,7 +37,7 @@ class HealthMonitorFlows(object): create_hm_flow.add(database_tasks.MarkHealthMonitorPendingCreateInDB( requires=constants.HEALTH_MON)) create_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_hm_flow.add(database_tasks.MarkHealthMonitorActiveInDB( requires=constants.HEALTH_MON)) create_hm_flow.add(database_tasks.MarkPoolActiveInDB( @@ -63,7 +63,7 @@ class HealthMonitorFlows(object): DeleteModelObject(rebind={constants.OBJECT: constants.HEALTH_MON})) delete_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_hm_flow.add(database_tasks.DeleteHealthMonitorInDB( requires=constants.HEALTH_MON)) delete_hm_flow.add(database_tasks.DecrementHealthMonitorQuota( @@ -92,7 +92,7 @@ class HealthMonitorFlows(object): update_hm_flow.add(database_tasks.MarkHealthMonitorPendingUpdateInDB( requires=constants.HEALTH_MON)) update_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_hm_flow.add(database_tasks.UpdateHealthMonInDB( requires=[constants.HEALTH_MON, constants.UPDATE_DICT])) update_hm_flow.add(database_tasks.MarkHealthMonitorActiveInDB( diff --git a/octavia/controller/worker/v1/flows/l7policy_flows.py b/octavia/controller/worker/v1/flows/l7policy_flows.py index 59a1890ed7..a74dfb55e0 100644 --- a/octavia/controller/worker/v1/flows/l7policy_flows.py +++ b/octavia/controller/worker/v1/flows/l7policy_flows.py @@ -37,7 +37,7 @@ class L7PolicyFlows(object): create_l7policy_flow.add(database_tasks.MarkL7PolicyPendingCreateInDB( requires=constants.L7POLICY)) create_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_l7policy_flow.add(database_tasks.MarkL7PolicyActiveInDB( requires=constants.L7POLICY)) create_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -60,7 +60,7 @@ class L7PolicyFlows(object): delete_l7policy_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.L7POLICY})) delete_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_l7policy_flow.add(database_tasks.DeleteL7PolicyInDB( requires=constants.L7POLICY)) delete_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -81,7 +81,7 @@ class L7PolicyFlows(object): update_l7policy_flow.add(database_tasks.MarkL7PolicyPendingUpdateInDB( requires=constants.L7POLICY)) update_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_l7policy_flow.add(database_tasks.UpdateL7PolicyInDB( requires=[constants.L7POLICY, constants.UPDATE_DICT])) update_l7policy_flow.add(database_tasks.MarkL7PolicyActiveInDB( diff --git a/octavia/controller/worker/v1/flows/l7rule_flows.py b/octavia/controller/worker/v1/flows/l7rule_flows.py index 62ab8c72e4..2cb234ea85 100644 --- a/octavia/controller/worker/v1/flows/l7rule_flows.py +++ b/octavia/controller/worker/v1/flows/l7rule_flows.py @@ -37,7 +37,7 @@ class L7RuleFlows(object): create_l7rule_flow.add(database_tasks.MarkL7RulePendingCreateInDB( requires=constants.L7RULE)) create_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_l7rule_flow.add(database_tasks.MarkL7RuleActiveInDB( requires=constants.L7RULE)) create_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB( @@ -62,7 +62,7 @@ class L7RuleFlows(object): delete_l7rule_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.L7RULE})) delete_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_l7rule_flow.add(database_tasks.DeleteL7RuleInDB( requires=constants.L7RULE)) delete_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB( @@ -85,7 +85,7 @@ class L7RuleFlows(object): update_l7rule_flow.add(database_tasks.MarkL7RulePendingUpdateInDB( requires=constants.L7RULE)) update_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_l7rule_flow.add(database_tasks.UpdateL7RuleInDB( requires=[constants.L7RULE, constants.UPDATE_DICT])) update_l7rule_flow.add(database_tasks.MarkL7RuleActiveInDB( diff --git a/octavia/controller/worker/v1/flows/listener_flows.py b/octavia/controller/worker/v1/flows/listener_flows.py index 43d7903689..01125364e8 100644 --- a/octavia/controller/worker/v1/flows/listener_flows.py +++ b/octavia/controller/worker/v1/flows/listener_flows.py @@ -33,7 +33,7 @@ class ListenerFlows(object): create_listener_flow.add(lifecycle_tasks.ListenersToErrorOnRevertTask( requires=[constants.LOADBALANCER, constants.LISTENERS])) create_listener_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_listener_flow.add(network_tasks.UpdateVIP( requires=constants.LOADBALANCER)) create_listener_flow.add(database_tasks. @@ -57,7 +57,7 @@ class ListenerFlows(object): requires=constants.LOADBALANCER_ID, provides=constants.LOADBALANCER)) create_all_listeners_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_all_listeners_flow.add(network_tasks.UpdateVIP( requires=constants.LOADBALANCER)) return create_all_listeners_flow @@ -71,7 +71,7 @@ class ListenerFlows(object): delete_listener_flow.add(lifecycle_tasks.ListenerToErrorOnRevertTask( requires=constants.LISTENER)) delete_listener_flow.add(amphora_driver_tasks.ListenerDelete( - requires=[constants.LOADBALANCER, constants.LISTENER])) + requires=constants.LISTENER)) delete_listener_flow.add(network_tasks.UpdateVIPForDelete( requires=constants.LOADBALANCER)) delete_listener_flow.add(database_tasks.DeleteListenerInDB( @@ -115,7 +115,7 @@ class ListenerFlows(object): update_listener_flow.add(lifecycle_tasks.ListenersToErrorOnRevertTask( requires=[constants.LOADBALANCER, constants.LISTENERS])) update_listener_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_listener_flow.add(database_tasks.UpdateListenerInDB( requires=[constants.LISTENER, constants.UPDATE_DICT])) update_listener_flow.add(database_tasks. diff --git a/octavia/controller/worker/v1/flows/load_balancer_flows.py b/octavia/controller/worker/v1/flows/load_balancer_flows.py index 96abd55c0b..82478bd623 100644 --- a/octavia/controller/worker/v1/flows/load_balancer_flows.py +++ b/octavia/controller/worker/v1/flows/load_balancer_flows.py @@ -332,7 +332,7 @@ class LoadBalancerFlows(object): update_LB_flow.add(network_tasks.ApplyQos( requires=(constants.LOADBALANCER, constants.UPDATE_DICT))) update_LB_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_LB_flow.add(database_tasks.UpdateLoadbalancerInDB( requires=[constants.LOADBALANCER, constants.UPDATE_DICT])) update_LB_flow.add(database_tasks.MarkLBActiveInDB( diff --git a/octavia/controller/worker/v1/flows/member_flows.py b/octavia/controller/worker/v1/flows/member_flows.py index dfa32683c8..deefd88eec 100644 --- a/octavia/controller/worker/v1/flows/member_flows.py +++ b/octavia/controller/worker/v1/flows/member_flows.py @@ -48,7 +48,7 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.ADDED_PORTS) )) create_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=(constants.LOADBALANCER, constants.LISTENERS))) + requires=constants.LOADBALANCER)) create_member_flow.add(database_tasks.MarkMemberActiveInDB( requires=constants.MEMBER)) create_member_flow.add(database_tasks.MarkPoolActiveInDB( @@ -79,7 +79,7 @@ class MemberFlows(object): delete_member_flow.add(database_tasks.DeleteMemberInDB( requires=constants.MEMBER)) delete_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_member_flow.add(database_tasks.DecrementMemberQuota( requires=constants.MEMBER)) delete_member_flow.add(database_tasks.MarkPoolActiveInDB( @@ -105,7 +105,7 @@ class MemberFlows(object): update_member_flow.add(database_tasks.MarkMemberPendingUpdateInDB( requires=constants.MEMBER)) update_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_member_flow.add(database_tasks.UpdateMemberInDB( requires=[constants.MEMBER, constants.UPDATE_DICT])) update_member_flow.add(database_tasks.MarkMemberActiveInDB( @@ -195,7 +195,7 @@ class MemberFlows(object): # Update the Listener (this makes the changes active on the Amp) batch_update_members_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=(constants.LOADBALANCER, constants.LISTENERS))) + requires=constants.LOADBALANCER)) # Mark all the members ACTIVE here, then pool then LB/Listeners batch_update_members_flow.add(unordered_members_active_flow) diff --git a/octavia/controller/worker/v1/flows/pool_flows.py b/octavia/controller/worker/v1/flows/pool_flows.py index 96dadb989b..78c67ebdfd 100644 --- a/octavia/controller/worker/v1/flows/pool_flows.py +++ b/octavia/controller/worker/v1/flows/pool_flows.py @@ -37,7 +37,7 @@ class PoolFlows(object): create_pool_flow.add(database_tasks.MarkPoolPendingCreateInDB( requires=constants.POOL)) create_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_pool_flow.add(database_tasks.MarkPoolActiveInDB( requires=constants.POOL)) create_pool_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -62,7 +62,7 @@ class PoolFlows(object): delete_pool_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.POOL})) delete_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_pool_flow.add(database_tasks.DeletePoolInDB( requires=constants.POOL)) delete_pool_flow.add(database_tasks.DecrementPoolQuota( @@ -116,7 +116,7 @@ class PoolFlows(object): update_pool_flow.add(database_tasks.MarkPoolPendingUpdateInDB( requires=constants.POOL)) update_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_pool_flow.add(database_tasks.UpdatePoolInDB( requires=[constants.POOL, constants.UPDATE_DICT])) update_pool_flow.add(database_tasks.MarkPoolActiveInDB( diff --git a/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py b/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py index 6fa16015c5..5a2f07683d 100644 --- a/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py +++ b/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py @@ -52,13 +52,13 @@ class BaseAmphoraTask(task.Task): class AmpListenersUpdate(BaseAmphoraTask): """Task to update the listeners on one amphora.""" - def execute(self, listeners, amphora_index, amphorae, timeout_dict=()): + def execute(self, loadbalancer, amphora_index, amphorae, timeout_dict=()): # Note, we don't want this to cause a revert as it may be used # in a failover flow with both amps failing. Skip it and let # health manager fix it. try: self.amphora_driver.update_amphora_listeners( - listeners, amphora_index, amphorae, timeout_dict) + loadbalancer, amphorae[amphora_index], timeout_dict) except Exception as e: amphora_id = amphorae[amphora_index].id LOG.error('Failed to update listeners on amphora %s. Skipping ' @@ -71,11 +71,9 @@ class AmpListenersUpdate(BaseAmphoraTask): class ListenersUpdate(BaseAmphoraTask): """Task to update amphora with all specified listeners' configurations.""" - def execute(self, loadbalancer, listeners): + def execute(self, loadbalancer): """Execute updates per listener for an amphora.""" - for listener in listeners: - listener.load_balancer = loadbalancer - self.amphora_driver.update(listener, loadbalancer.vip) + self.amphora_driver.update(loadbalancer) def revert(self, loadbalancer, *args, **kwargs): """Handle failed listeners updates.""" @@ -86,61 +84,30 @@ class ListenersUpdate(BaseAmphoraTask): self.task_utils.mark_listener_prov_status_error(listener.id) -class ListenerStop(BaseAmphoraTask): - """Task to stop the listener on the vip.""" - - def execute(self, loadbalancer, listener): - """Execute listener stop routines for an amphora.""" - self.amphora_driver.stop(listener, loadbalancer.vip) - LOG.debug("Stopped the listener on the vip") - - def revert(self, listener, *args, **kwargs): - """Handle a failed listener stop.""" - - LOG.warning("Reverting listener stop.") - - self.task_utils.mark_listener_prov_status_error(listener.id) - - -class ListenerStart(BaseAmphoraTask): - """Task to start the listener on the vip.""" - - def execute(self, loadbalancer, listener): - """Execute listener start routines for an amphora.""" - self.amphora_driver.start(listener, loadbalancer.vip) - LOG.debug("Started the listener on the vip") - - def revert(self, listener, *args, **kwargs): - """Handle a failed listener start.""" - - LOG.warning("Reverting listener start.") - - self.task_utils.mark_listener_prov_status_error(listener.id) - - class ListenersStart(BaseAmphoraTask): """Task to start all listeners on the vip.""" - def execute(self, loadbalancer, listeners, amphora=None): + def execute(self, loadbalancer, amphora=None): """Execute listener start routines for listeners on an amphora.""" - for listener in listeners: - self.amphora_driver.start(listener, loadbalancer.vip, amphora) - LOG.debug("Started the listeners on the vip") + if loadbalancer.listeners: + self.amphora_driver.start(loadbalancer, amphora) + LOG.debug("Started the listeners on the vip") - def revert(self, listeners, *args, **kwargs): + def revert(self, loadbalancer, *args, **kwargs): """Handle failed listeners starts.""" LOG.warning("Reverting listeners starts.") - for listener in listeners: + for listener in loadbalancer.listeners: self.task_utils.mark_listener_prov_status_error(listener.id) class ListenerDelete(BaseAmphoraTask): """Task to delete the listener on the vip.""" - def execute(self, loadbalancer, listener): + def execute(self, listener): """Execute listener delete routines for an amphora.""" - self.amphora_driver.delete(listener, loadbalancer.vip) + # TODO(rm_work): This is only relevant because of UDP listeners now. + self.amphora_driver.delete(listener) LOG.debug("Deleted the listener on the vip") def revert(self, listener, *args, **kwargs): diff --git a/octavia/controller/worker/v2/controller_worker.py b/octavia/controller/worker/v2/controller_worker.py index cacc909834..8c0de0625e 100644 --- a/octavia/controller/worker/v2/controller_worker.py +++ b/octavia/controller/worker/v2/controller_worker.py @@ -235,13 +235,14 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine): raise db_exceptions.NoResultFound load_balancer = listener.load_balancer + listeners = load_balancer.listeners create_listener_tf = self._taskflow_load(self._listener_flows. get_create_listener_flow(), store={constants.LOADBALANCER: load_balancer, constants.LISTENERS: - [listener]}) + listeners}) with tf_logging.DynamicLoggingListener(create_listener_tf, log=LOG): create_listener_tf.run() diff --git a/octavia/controller/worker/v2/flows/amphora_flows.py b/octavia/controller/worker/v2/flows/amphora_flows.py index b7e4338983..09deab886e 100644 --- a/octavia/controller/worker/v2/flows/amphora_flows.py +++ b/octavia/controller/worker/v2/flows/amphora_flows.py @@ -470,7 +470,7 @@ class AmphoraFlows(object): update_amps_subflow.add( amphora_driver_tasks.AmpListenersUpdate( name=constants.AMP_LISTENER_UPDATE + '-' + str(amp_index), - requires=(constants.LISTENERS, constants.AMPHORAE), + requires=(constants.LOADBALANCER, constants.AMPHORAE), inject={constants.AMPHORA_INDEX: amp_index, constants.TIMEOUT_DICT: timeout_dict})) amp_index += 1 @@ -514,8 +514,7 @@ class AmphoraFlows(object): requires=constants.AMPHORA)) failover_amphora_flow.add(amphora_driver_tasks.ListenersStart( - requires=(constants.LOADBALANCER, constants.LISTENERS, - constants.AMPHORA))) + requires=(constants.LOADBALANCER, constants.AMPHORA))) failover_amphora_flow.add( database_tasks.DisableAmphoraHealthMonitoring( rebind={constants.AMPHORA: constants.FAILED_AMPHORA}, diff --git a/octavia/controller/worker/v2/flows/health_monitor_flows.py b/octavia/controller/worker/v2/flows/health_monitor_flows.py index 2fc14f1324..f2e2ad8c5f 100644 --- a/octavia/controller/worker/v2/flows/health_monitor_flows.py +++ b/octavia/controller/worker/v2/flows/health_monitor_flows.py @@ -37,7 +37,7 @@ class HealthMonitorFlows(object): create_hm_flow.add(database_tasks.MarkHealthMonitorPendingCreateInDB( requires=constants.HEALTH_MON)) create_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_hm_flow.add(database_tasks.MarkHealthMonitorActiveInDB( requires=constants.HEALTH_MON)) create_hm_flow.add(database_tasks.MarkPoolActiveInDB( @@ -63,7 +63,7 @@ class HealthMonitorFlows(object): DeleteModelObject(rebind={constants.OBJECT: constants.HEALTH_MON})) delete_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_hm_flow.add(database_tasks.DeleteHealthMonitorInDB( requires=constants.HEALTH_MON)) delete_hm_flow.add(database_tasks.DecrementHealthMonitorQuota( @@ -92,7 +92,7 @@ class HealthMonitorFlows(object): update_hm_flow.add(database_tasks.MarkHealthMonitorPendingUpdateInDB( requires=constants.HEALTH_MON)) update_hm_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_hm_flow.add(database_tasks.UpdateHealthMonInDB( requires=[constants.HEALTH_MON, constants.UPDATE_DICT])) update_hm_flow.add(database_tasks.MarkHealthMonitorActiveInDB( diff --git a/octavia/controller/worker/v2/flows/l7policy_flows.py b/octavia/controller/worker/v2/flows/l7policy_flows.py index 98e11b2584..e988c93a39 100644 --- a/octavia/controller/worker/v2/flows/l7policy_flows.py +++ b/octavia/controller/worker/v2/flows/l7policy_flows.py @@ -37,7 +37,7 @@ class L7PolicyFlows(object): create_l7policy_flow.add(database_tasks.MarkL7PolicyPendingCreateInDB( requires=constants.L7POLICY)) create_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_l7policy_flow.add(database_tasks.MarkL7PolicyActiveInDB( requires=constants.L7POLICY)) create_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -60,7 +60,7 @@ class L7PolicyFlows(object): delete_l7policy_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.L7POLICY})) delete_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_l7policy_flow.add(database_tasks.DeleteL7PolicyInDB( requires=constants.L7POLICY)) delete_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -81,7 +81,7 @@ class L7PolicyFlows(object): update_l7policy_flow.add(database_tasks.MarkL7PolicyPendingUpdateInDB( requires=constants.L7POLICY)) update_l7policy_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_l7policy_flow.add(database_tasks.UpdateL7PolicyInDB( requires=[constants.L7POLICY, constants.UPDATE_DICT])) update_l7policy_flow.add(database_tasks.MarkL7PolicyActiveInDB( diff --git a/octavia/controller/worker/v2/flows/l7rule_flows.py b/octavia/controller/worker/v2/flows/l7rule_flows.py index 7f828f8f9e..48a01bd346 100644 --- a/octavia/controller/worker/v2/flows/l7rule_flows.py +++ b/octavia/controller/worker/v2/flows/l7rule_flows.py @@ -37,7 +37,7 @@ class L7RuleFlows(object): create_l7rule_flow.add(database_tasks.MarkL7RulePendingCreateInDB( requires=constants.L7RULE)) create_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_l7rule_flow.add(database_tasks.MarkL7RuleActiveInDB( requires=constants.L7RULE)) create_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB( @@ -62,7 +62,7 @@ class L7RuleFlows(object): delete_l7rule_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.L7RULE})) delete_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_l7rule_flow.add(database_tasks.DeleteL7RuleInDB( requires=constants.L7RULE)) delete_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB( @@ -85,7 +85,7 @@ class L7RuleFlows(object): update_l7rule_flow.add(database_tasks.MarkL7RulePendingUpdateInDB( requires=constants.L7RULE)) update_l7rule_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_l7rule_flow.add(database_tasks.UpdateL7RuleInDB( requires=[constants.L7RULE, constants.UPDATE_DICT])) update_l7rule_flow.add(database_tasks.MarkL7RuleActiveInDB( diff --git a/octavia/controller/worker/v2/flows/listener_flows.py b/octavia/controller/worker/v2/flows/listener_flows.py index d02cd03ac4..ebb1dd4fe6 100644 --- a/octavia/controller/worker/v2/flows/listener_flows.py +++ b/octavia/controller/worker/v2/flows/listener_flows.py @@ -33,7 +33,7 @@ class ListenerFlows(object): create_listener_flow.add(lifecycle_tasks.ListenersToErrorOnRevertTask( requires=[constants.LOADBALANCER, constants.LISTENERS])) create_listener_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_listener_flow.add(network_tasks.UpdateVIP( requires=constants.LOADBALANCER)) create_listener_flow.add(database_tasks. @@ -57,7 +57,7 @@ class ListenerFlows(object): requires=constants.LOADBALANCER_ID, provides=constants.LOADBALANCER)) create_all_listeners_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_all_listeners_flow.add(network_tasks.UpdateVIP( requires=constants.LOADBALANCER)) return create_all_listeners_flow @@ -71,7 +71,7 @@ class ListenerFlows(object): delete_listener_flow.add(lifecycle_tasks.ListenerToErrorOnRevertTask( requires=constants.LISTENER)) delete_listener_flow.add(amphora_driver_tasks.ListenerDelete( - requires=[constants.LOADBALANCER, constants.LISTENER])) + requires=constants.LISTENER)) delete_listener_flow.add(network_tasks.UpdateVIPForDelete( requires=constants.LOADBALANCER)) delete_listener_flow.add(database_tasks.DeleteListenerInDB( @@ -115,7 +115,7 @@ class ListenerFlows(object): update_listener_flow.add(lifecycle_tasks.ListenersToErrorOnRevertTask( requires=[constants.LOADBALANCER, constants.LISTENERS])) update_listener_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_listener_flow.add(database_tasks.UpdateListenerInDB( requires=[constants.LISTENER, constants.UPDATE_DICT])) update_listener_flow.add(database_tasks. diff --git a/octavia/controller/worker/v2/flows/load_balancer_flows.py b/octavia/controller/worker/v2/flows/load_balancer_flows.py index 968a719090..a09b534bf9 100644 --- a/octavia/controller/worker/v2/flows/load_balancer_flows.py +++ b/octavia/controller/worker/v2/flows/load_balancer_flows.py @@ -332,7 +332,7 @@ class LoadBalancerFlows(object): update_LB_flow.add(network_tasks.ApplyQos( requires=(constants.LOADBALANCER, constants.UPDATE_DICT))) update_LB_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_LB_flow.add(database_tasks.UpdateLoadbalancerInDB( requires=[constants.LOADBALANCER, constants.UPDATE_DICT])) update_LB_flow.add(database_tasks.MarkLBActiveInDB( diff --git a/octavia/controller/worker/v2/flows/member_flows.py b/octavia/controller/worker/v2/flows/member_flows.py index 2e6829d3a9..ff1f4a402d 100644 --- a/octavia/controller/worker/v2/flows/member_flows.py +++ b/octavia/controller/worker/v2/flows/member_flows.py @@ -48,7 +48,7 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.ADDED_PORTS) )) create_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=(constants.LOADBALANCER, constants.LISTENERS))) + requires=constants.LOADBALANCER)) create_member_flow.add(database_tasks.MarkMemberActiveInDB( requires=constants.MEMBER)) create_member_flow.add(database_tasks.MarkPoolActiveInDB( @@ -79,7 +79,7 @@ class MemberFlows(object): delete_member_flow.add(database_tasks.DeleteMemberInDB( requires=constants.MEMBER)) delete_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_member_flow.add(database_tasks.DecrementMemberQuota( requires=constants.MEMBER)) delete_member_flow.add(database_tasks.MarkPoolActiveInDB( @@ -105,7 +105,7 @@ class MemberFlows(object): update_member_flow.add(database_tasks.MarkMemberPendingUpdateInDB( requires=constants.MEMBER)) update_member_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_member_flow.add(database_tasks.UpdateMemberInDB( requires=[constants.MEMBER, constants.UPDATE_DICT])) update_member_flow.add(database_tasks.MarkMemberActiveInDB( @@ -195,7 +195,7 @@ class MemberFlows(object): # Update the Listener (this makes the changes active on the Amp) batch_update_members_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=(constants.LOADBALANCER, constants.LISTENERS))) + requires=constants.LOADBALANCER)) # Mark all the members ACTIVE here, then pool then LB/Listeners batch_update_members_flow.add(unordered_members_active_flow) diff --git a/octavia/controller/worker/v2/flows/pool_flows.py b/octavia/controller/worker/v2/flows/pool_flows.py index 0cf615be82..71b8482bfe 100644 --- a/octavia/controller/worker/v2/flows/pool_flows.py +++ b/octavia/controller/worker/v2/flows/pool_flows.py @@ -37,7 +37,7 @@ class PoolFlows(object): create_pool_flow.add(database_tasks.MarkPoolPendingCreateInDB( requires=constants.POOL)) create_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) create_pool_flow.add(database_tasks.MarkPoolActiveInDB( requires=constants.POOL)) create_pool_flow.add(database_tasks.MarkLBAndListenersActiveInDB( @@ -62,7 +62,7 @@ class PoolFlows(object): delete_pool_flow.add(model_tasks.DeleteModelObject( rebind={constants.OBJECT: constants.POOL})) delete_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) delete_pool_flow.add(database_tasks.DeletePoolInDB( requires=constants.POOL)) delete_pool_flow.add(database_tasks.DecrementPoolQuota( @@ -116,7 +116,7 @@ class PoolFlows(object): update_pool_flow.add(database_tasks.MarkPoolPendingUpdateInDB( requires=constants.POOL)) update_pool_flow.add(amphora_driver_tasks.ListenersUpdate( - requires=[constants.LOADBALANCER, constants.LISTENERS])) + requires=constants.LOADBALANCER)) update_pool_flow.add(database_tasks.UpdatePoolInDB( requires=[constants.POOL, constants.UPDATE_DICT])) update_pool_flow.add(database_tasks.MarkPoolActiveInDB( diff --git a/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py b/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py index 6fa16015c5..5a2f07683d 100644 --- a/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py +++ b/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py @@ -52,13 +52,13 @@ class BaseAmphoraTask(task.Task): class AmpListenersUpdate(BaseAmphoraTask): """Task to update the listeners on one amphora.""" - def execute(self, listeners, amphora_index, amphorae, timeout_dict=()): + def execute(self, loadbalancer, amphora_index, amphorae, timeout_dict=()): # Note, we don't want this to cause a revert as it may be used # in a failover flow with both amps failing. Skip it and let # health manager fix it. try: self.amphora_driver.update_amphora_listeners( - listeners, amphora_index, amphorae, timeout_dict) + loadbalancer, amphorae[amphora_index], timeout_dict) except Exception as e: amphora_id = amphorae[amphora_index].id LOG.error('Failed to update listeners on amphora %s. Skipping ' @@ -71,11 +71,9 @@ class AmpListenersUpdate(BaseAmphoraTask): class ListenersUpdate(BaseAmphoraTask): """Task to update amphora with all specified listeners' configurations.""" - def execute(self, loadbalancer, listeners): + def execute(self, loadbalancer): """Execute updates per listener for an amphora.""" - for listener in listeners: - listener.load_balancer = loadbalancer - self.amphora_driver.update(listener, loadbalancer.vip) + self.amphora_driver.update(loadbalancer) def revert(self, loadbalancer, *args, **kwargs): """Handle failed listeners updates.""" @@ -86,61 +84,30 @@ class ListenersUpdate(BaseAmphoraTask): self.task_utils.mark_listener_prov_status_error(listener.id) -class ListenerStop(BaseAmphoraTask): - """Task to stop the listener on the vip.""" - - def execute(self, loadbalancer, listener): - """Execute listener stop routines for an amphora.""" - self.amphora_driver.stop(listener, loadbalancer.vip) - LOG.debug("Stopped the listener on the vip") - - def revert(self, listener, *args, **kwargs): - """Handle a failed listener stop.""" - - LOG.warning("Reverting listener stop.") - - self.task_utils.mark_listener_prov_status_error(listener.id) - - -class ListenerStart(BaseAmphoraTask): - """Task to start the listener on the vip.""" - - def execute(self, loadbalancer, listener): - """Execute listener start routines for an amphora.""" - self.amphora_driver.start(listener, loadbalancer.vip) - LOG.debug("Started the listener on the vip") - - def revert(self, listener, *args, **kwargs): - """Handle a failed listener start.""" - - LOG.warning("Reverting listener start.") - - self.task_utils.mark_listener_prov_status_error(listener.id) - - class ListenersStart(BaseAmphoraTask): """Task to start all listeners on the vip.""" - def execute(self, loadbalancer, listeners, amphora=None): + def execute(self, loadbalancer, amphora=None): """Execute listener start routines for listeners on an amphora.""" - for listener in listeners: - self.amphora_driver.start(listener, loadbalancer.vip, amphora) - LOG.debug("Started the listeners on the vip") + if loadbalancer.listeners: + self.amphora_driver.start(loadbalancer, amphora) + LOG.debug("Started the listeners on the vip") - def revert(self, listeners, *args, **kwargs): + def revert(self, loadbalancer, *args, **kwargs): """Handle failed listeners starts.""" LOG.warning("Reverting listeners starts.") - for listener in listeners: + for listener in loadbalancer.listeners: self.task_utils.mark_listener_prov_status_error(listener.id) class ListenerDelete(BaseAmphoraTask): """Task to delete the listener on the vip.""" - def execute(self, loadbalancer, listener): + def execute(self, listener): """Execute listener delete routines for an amphora.""" - self.amphora_driver.delete(listener, loadbalancer.vip) + # TODO(rm_work): This is only relevant because of UDP listeners now. + self.amphora_driver.delete(listener) LOG.debug("Deleted the listener on the vip") def revert(self, listener, *args, **kwargs): diff --git a/octavia/db/repositories.py b/octavia/db/repositories.py index 5a01c7ad68..f697f13f02 100644 --- a/octavia/db/repositories.py +++ b/octavia/db/repositories.py @@ -1253,6 +1253,7 @@ class AmphoraRepository(BaseRepository): "load_balancer.operating_status AS lb_op_status, " "listener.id AS list_id, " "listener.operating_status AS list_op_status, " + "listener.enabled AS list_enabled, " "listener.protocol AS list_protocol, " "pool.id AS pool_id, " "pool.operating_status AS pool_op_status, " @@ -1278,7 +1279,8 @@ class AmphoraRepository(BaseRepository): lb['operating_status'] = row['lb_op_status'] if row['list_id'] and row['list_id'] not in listeners: listener = {'operating_status': row['list_op_status'], - 'protocol': row['list_protocol']} + 'protocol': row['list_protocol'], + 'enabled': row['list_enabled']} listeners[row['list_id']] = listener if row['pool_id']: if row['pool_id'] in pools and row['member_id']: diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_keepalivedlvs.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_keepalivedlvs.py index 2646118a8e..41ca20a64a 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_keepalivedlvs.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_keepalivedlvs.py @@ -18,7 +18,6 @@ import subprocess import flask import mock -from werkzeug import exceptions from oslo_utils import uuidutils @@ -315,67 +314,6 @@ class KeepalivedLvsTestCase(base.TestCase): 'start') self.assertEqual(500, res.status_code) - @mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.' - 'get_listener_realserver_mapping') - @mock.patch('subprocess.check_output', return_value=PROC_CONTENT) - @mock.patch('os.path.exists') - def test_get_udp_listener_status(self, m_exist, m_check_output, - mget_mapping): - mget_mapping.return_value = ( - True, {'10.0.0.99:82': {'status': 'UP', - 'Weight': '13', - 'InActConn': '0', - 'ActiveConn': '0'}, - '10.0.0.98:82': {'status': 'UP', - 'Weight': '13', - 'InActConn': '0', - 'ActiveConn': '0'}}) - pid_path = ('/var/lib/octavia/lvs/octavia-' - 'keepalivedlvs-%s.pid' % self.FAKE_ID) - self.useFixture(test_utils.OpenFixture(pid_path, - self.NORMAL_PID_CONTENT)) - - cfg_path = ('/var/lib/octavia/lvs/octavia-' - 'keepalivedlvs-%s.conf' % self.FAKE_ID) - self.useFixture(test_utils.OpenFixture(cfg_path, - self.NORMAL_CFG_CONTENT)) - - m_exist.return_value = True - expected = {'status': 'ACTIVE', - 'pools': [{'lvs': { - 'members': {self.MEMBER_ID1: 'UP', - self.MEMBER_ID2: 'UP'}, - 'status': 'UP', - 'uuid': self.POOL_ID}}], - 'type': 'UDP', 'uuid': self.FAKE_ID} - res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID) - self.assertEqual(200, res.status_code) - self.assertEqual(expected, res.json) - - @mock.patch('os.path.exists') - def test_get_udp_listener_status_no_exists(self, m_exist): - m_exist.return_value = False - self.assertRaises(exceptions.HTTPException, - self.test_keepalivedlvs.get_udp_listener_status, - self.FAKE_ID) - - @mock.patch('os.path.exists') - def test_get_udp_listener_status_offline_status(self, m_exist): - m_exist.return_value = True - pid_path = ('/var/lib/octavia/lvs/octavia-' - 'keepalivedlvs-%s.pid' % self.FAKE_ID) - self.useFixture(test_utils.OpenFixture(pid_path, - self.NORMAL_PID_CONTENT)) - cfg_path = ('/var/lib/octavia/lvs/octavia-' - 'keepalivedlvs-%s.conf' % self.FAKE_ID) - self.useFixture(test_utils.OpenFixture(cfg_path, 'NO VS CONFIG')) - expected = {'status': 'OFFLINE', - 'type': 'UDP', - 'uuid': self.FAKE_ID} - res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID) - self.assertEqual(200, res.status_code) - self.assertEqual(expected, res.json) - @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_udp_listeners', return_value=[LISTENER_ID]) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index 379b21c241..31931de9e2 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -118,11 +118,11 @@ class TestServerTestCase(base.TestCase): mock_distro_id.return_value = distro if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') mode = stat.S_IRUSR | stat.S_IWUSR mock_open.assert_called_with(file_name, flags, mode) @@ -159,11 +159,11 @@ class TestServerTestCase(base.TestCase): mock_distro_id.return_value = distro if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') self.assertEqual(500, rv.status_code) @@ -188,11 +188,11 @@ class TestServerTestCase(base.TestCase): if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') self.assertEqual(202, rv.status_code) @@ -220,11 +220,11 @@ class TestServerTestCase(base.TestCase): mock_distro_id.return_value = distro if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') self.assertEqual(400, rv.status_code) self.assertEqual( @@ -255,11 +255,11 @@ class TestServerTestCase(base.TestCase): mock_distro_id.return_value = distro if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') elif distro == consts.CENTOS: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', + '/loadbalancer/amp_123/123/haproxy', data='test') self.assertEqual(500, rv.status_code) @@ -269,45 +269,49 @@ class TestServerTestCase(base.TestCase): def test_centos_start(self): self._test_start(consts.CENTOS) + @mock.patch('os.listdir') @mock.patch('os.path.exists') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - 'vrrp_check_script_update') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer.vrrp_check_script_update') @mock.patch('subprocess.check_output') - def _test_start(self, distro, mock_subprocess, mock_vrrp, mock_exists): + def _test_start(self, distro, mock_subprocess, mock_vrrp, mock_exists, + mock_listdir): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/error') + '/loadbalancer/123/error') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/error') + '/loadbalancer/123/error') self.assertEqual(400, rv.status_code) self.assertEqual( {'message': 'Invalid Request', 'details': 'Unknown action: error', }, jsonutils.loads(rv.data.decode('utf-8'))) + mock_exists.reset_mock() mock_exists.return_value = False if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') self.assertEqual(404, rv.status_code) self.assertEqual( - {'message': 'Listener Not Found', - 'details': 'No listener with UUID: 123'}, + {'message': 'Loadbalancer Not Found', + 'details': 'No loadbalancer with UUID: 123'}, jsonutils.loads(rv.data.decode('utf-8'))) - mock_exists.assert_called_with('/var/lib/octavia/123/haproxy.cfg') + mock_exists.assert_called_with('/var/lib/octavia') mock_exists.return_value = True + mock_listdir.return_value = ['123'] if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -322,10 +326,10 @@ class TestServerTestCase(base.TestCase): 7, 'test', RANDOM_ERROR) if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/start') + '/loadbalancer/123/start') self.assertEqual(500, rv.status_code) self.assertEqual( { @@ -341,26 +345,28 @@ class TestServerTestCase(base.TestCase): def test_centos_reload(self): self._test_reload(consts.CENTOS) + @mock.patch('os.listdir') @mock.patch('os.path.exists') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - 'vrrp_check_script_update') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_check_haproxy_status') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer.vrrp_check_script_update') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer._check_haproxy_status') @mock.patch('subprocess.check_output') def _test_reload(self, distro, mock_subprocess, mock_haproxy_status, - mock_vrrp, mock_exists): + mock_vrrp, mock_exists, mock_listdir): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # Process running so reload mock_exists.return_value = True + mock_listdir.return_value = ['123'] mock_haproxy_status.return_value = consts.ACTIVE if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/reload') + '/loadbalancer/123/reload') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/reload') + '/loadbalancer/123/reload') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -374,10 +380,10 @@ class TestServerTestCase(base.TestCase): mock_haproxy_status.return_value = consts.OFFLINE if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/reload') + '/loadbalancer/123/reload') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/reload') + '/loadbalancer/123/reload') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -411,13 +417,13 @@ class TestServerTestCase(base.TestCase): self.assertEqual(200, rv.status_code) self.assertEqual(dict( - api_version='0.5', + api_version='1.0', haproxy_version='9.9.99-9', hostname='test-host'), jsonutils.loads(rv.data.decode('utf-8'))) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listener_protocol', return_value='TCP') + 'get_protocol_for_lb_object', return_value='TCP') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) def test_delete_ubuntu_listener_systemd(self, mock_init_system, @@ -426,7 +432,7 @@ class TestServerTestCase(base.TestCase): mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listener_protocol', return_value='TCP') + 'get_protocol_for_lb_object', return_value='TCP') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) def test_delete_centos_listener_systemd(self, mock_init_system, @@ -435,7 +441,7 @@ class TestServerTestCase(base.TestCase): mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listener_protocol', return_value='TCP') + 'get_protocol_for_lb_object', return_value='TCP') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSVINIT) def test_delete_ubuntu_listener_sysvinit(self, mock_init_system, @@ -444,7 +450,7 @@ class TestServerTestCase(base.TestCase): mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listener_protocol', return_value='TCP') + 'get_protocol_for_lb_object', return_value='TCP') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_UPSTART) def test_delete_ubuntu_listener_upstart(self, mock_init_system, @@ -452,20 +458,22 @@ class TestServerTestCase(base.TestCase): self._test_delete_listener(consts.INIT_UPSTART, consts.UBUNTU, mock_init_system) + @mock.patch('os.listdir') @mock.patch('os.path.exists') @mock.patch('subprocess.check_output') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - 'vrrp_check_script_update') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer.vrrp_check_script_update') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_haproxy_pid') @mock.patch('shutil.rmtree') @mock.patch('os.remove') def _test_delete_listener(self, init_system, distro, mock_init_system, mock_remove, mock_rmtree, mock_pid, mock_vrrp, - mock_check_output, mock_exists): + mock_check_output, mock_exists, mock_listdir): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # no listener mock_exists.return_value = False + mock_listdir.return_value = ['123'] if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + '/listeners/123') @@ -474,10 +482,10 @@ class TestServerTestCase(base.TestCase): '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual(OK, jsonutils.loads(rv.data.decode('utf-8'))) - mock_exists.assert_called_with('/var/lib/octavia/123/haproxy.cfg') + mock_exists.assert_called_once_with('/var/lib/octavia') # service is stopped + no upstart script + no vrrp - mock_exists.side_effect = [True, False, False, False] + mock_exists.side_effect = [True, True, False, False, False] if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + '/listeners/123') @@ -504,7 +512,7 @@ class TestServerTestCase(base.TestCase): mock_exists.assert_any_call('/var/lib/octavia/123/123.pid') # service is stopped + no upstart script + vrrp - mock_exists.side_effect = [True, False, True, False] + mock_exists.side_effect = [True, True, False, True, False] if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + '/listeners/123') @@ -531,7 +539,7 @@ class TestServerTestCase(base.TestCase): mock_exists.assert_any_call('/var/lib/octavia/123/123.pid') # service is stopped + upstart script + no vrrp - mock_exists.side_effect = [True, False, False, True] + mock_exists.side_effect = [True, True, False, False, True] if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + '/listeners/123') @@ -555,7 +563,7 @@ class TestServerTestCase(base.TestCase): self.assertIn(init_system, consts.VALID_INIT_SYSTEMS) # service is stopped + upstart script + vrrp - mock_exists.side_effect = [True, False, True, True] + mock_exists.side_effect = [True, True, False, True, True] if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + '/listeners/123') @@ -579,7 +587,7 @@ class TestServerTestCase(base.TestCase): self.assertIn(init_system, consts.VALID_INIT_SYSTEMS) # service is running + upstart script + no vrrp - mock_exists.side_effect = [True, True, True, False, True] + mock_exists.side_effect = [True, True, True, True, False, True] mock_pid.return_value = '456' if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + @@ -609,7 +617,7 @@ class TestServerTestCase(base.TestCase): self.assertIn(init_system, consts.VALID_INIT_SYSTEMS) # service is running + upstart script + vrrp - mock_exists.side_effect = [True, True, True, True, True] + mock_exists.side_effect = [True, True, True, True, True, True] mock_pid.return_value = '456' if distro == consts.UBUNTU: rv = self.ubuntu_app.delete('/' + api_server.VERSION + @@ -639,7 +647,7 @@ class TestServerTestCase(base.TestCase): self.assertIn(init_system, consts.VALID_INIT_SYSTEMS) # service is running + stopping fails - mock_exists.side_effect = [True, True, True] + mock_exists.side_effect = [True, True, True, True] mock_check_output.side_effect = subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR) if distro == consts.UBUNTU: @@ -661,8 +669,9 @@ class TestServerTestCase(base.TestCase): def test_centos_get_haproxy(self): self._test_get_haproxy(consts.CENTOS) + @mock.patch('os.listdir') @mock.patch('os.path.exists') - def _test_get_haproxy(self, distro, mock_exists): + def _test_get_haproxy(self, distro, mock_exists, mock_listdir): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) @@ -670,23 +679,24 @@ class TestServerTestCase(base.TestCase): mock_exists.side_effect = [False] if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123/haproxy') + '/loadbalancer/123/haproxy') elif distro == consts.CENTOS: rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123/haproxy') + '/loadbalancer/123/haproxy') self.assertEqual(404, rv.status_code) - mock_exists.side_effect = [True] + mock_exists.side_effect = [True, True] path = util.config_path('123') self.useFixture(test_utils.OpenFixture(path, CONTENT)) + mock_listdir.return_value = ['123'] if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123/haproxy') + '/loadbalancer/123/haproxy') elif distro == consts.CENTOS: rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123/haproxy') + '/loadbalancer/123/haproxy') self.assertEqual(200, rv.status_code) self.assertEqual(six.b(CONTENT), rv.data) self.assertEqual('text/plain; charset=utf-8', @@ -699,17 +709,17 @@ class TestServerTestCase(base.TestCase): self._test_get_all_listeners(consts.CENTOS) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listeners') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_check_listener_status') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_parse_haproxy_file') + 'get_loadbalancers') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer._check_haproxy_status') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'parse_haproxy_file') def _test_get_all_listeners(self, distro, mock_parse, mock_status, - mock_listener): + mock_lbs): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # no listeners - mock_listener.side_effect = [[]] + mock_lbs.side_effect = [[]] if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') elif distro == consts.CENTOS: @@ -719,8 +729,8 @@ class TestServerTestCase(base.TestCase): self.assertFalse(jsonutils.loads(rv.data.decode('utf-8'))) # one listener ACTIVE - mock_listener.side_effect = [['123']] - mock_parse.side_effect = [{'mode': 'test'}] + mock_lbs.side_effect = [['123']] + mock_parse.side_effect = [['fake_socket', {'123': {'mode': 'test'}}]] mock_status.side_effect = [consts.ACTIVE] if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') @@ -732,10 +742,11 @@ class TestServerTestCase(base.TestCase): [{'status': consts.ACTIVE, 'type': 'test', 'uuid': '123'}], jsonutils.loads(rv.data.decode('utf-8'))) - # two listener one ACTIVE, one ERROR - mock_listener.side_effect = [['123', '456']] - mock_parse.side_effect = [{'mode': 'test'}, {'mode': 'http'}] - mock_status.side_effect = [consts.ACTIVE, consts.ERROR] + # two listeners, two modes + mock_lbs.side_effect = [['123', '456']] + mock_parse.side_effect = [['fake_socket', {'123': {'mode': 'test'}}], + ['fake_socket', {'456': {'mode': 'http'}}]] + mock_status.return_value = consts.ACTIVE if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') elif distro == consts.CENTOS: @@ -744,86 +755,7 @@ class TestServerTestCase(base.TestCase): self.assertEqual(200, rv.status_code) self.assertEqual( [{'status': consts.ACTIVE, 'type': 'test', 'uuid': '123'}, - {'status': consts.ERROR, 'type': '', 'uuid': '456'}], - jsonutils.loads(rv.data.decode('utf-8'))) - - def test_ubuntu_get_listener(self): - self._test_get_listener(consts.UBUNTU) - - def test_centos_get_listener(self): - self._test_get_listener(consts.CENTOS) - - @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listener_protocol', return_value='TCP') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_check_listener_status') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_parse_haproxy_file') - @mock.patch('octavia.amphorae.backends.utils.haproxy_query.HAProxyQuery') - @mock.patch('os.path.exists') - def _test_get_listener(self, distro, mock_exists, mock_query, mock_parse, - mock_status, mock_get_proto): - self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) - # Listener not found - mock_exists.side_effect = [False] - if distro == consts.UBUNTU: - rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123') - elif distro == consts.CENTOS: - rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123') - self.assertEqual(404, rv.status_code) - self.assertEqual( - {'message': 'Listener Not Found', - 'details': 'No listener with UUID: 123'}, - jsonutils.loads(rv.data.decode('utf-8'))) - - # Listener not ACTIVE - mock_parse.side_effect = [dict(mode='test')] - mock_status.side_effect = [consts.ERROR] - mock_exists.side_effect = [True] - if distro == consts.UBUNTU: - rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123') - elif distro == consts.CENTOS: - rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123') - self.assertEqual(200, rv.status_code) - self.assertEqual(dict( - status=consts.ERROR, - type='', - uuid='123'), jsonutils.loads(rv.data.decode('utf-8'))) - - # Listener ACTIVE - mock_parse.side_effect = [dict(mode='test', stats_socket='blah')] - mock_status.side_effect = [consts.ACTIVE] - mock_exists.side_effect = [True] - mock_pool = mock.Mock() - mock_query.side_effect = [mock_pool] - mock_pool.get_pool_status.side_effect = [ - {'tcp-servers': { - 'status': 'DOWN', - 'uuid': 'tcp-servers', - 'members': [ - {'id-34833': 'DOWN'}, - {'id-34836': 'DOWN'}]}}] - if distro == consts.UBUNTU: - rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123') - elif distro == consts.CENTOS: - rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123') - self.assertEqual(200, rv.status_code) - self.assertEqual(dict( - status=consts.ACTIVE, - type='test', - uuid='123', - pools=[dict( - status=consts.DOWN, - uuid='tcp-servers', - members=[ - {u'id-34833': u'DOWN'}, - {u'id-34836': u'DOWN'}])]), + {'status': consts.ACTIVE, 'type': 'http', 'uuid': '456'}], jsonutils.loads(rv.data.decode('utf-8'))) def test_ubuntu_delete_cert(self): @@ -838,11 +770,13 @@ class TestServerTestCase(base.TestCase): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) mock_exists.side_effect = [False] if distro == consts.UBUNTU: - rv = self.ubuntu_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + rv = self.ubuntu_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.pem') elif distro == consts.CENTOS: - rv = self.centos_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + rv = self.centos_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.pem') self.assertEqual(200, rv.status_code) self.assertEqual(OK, jsonutils.loads(rv.data.decode('utf-8'))) mock_exists.assert_called_once_with( @@ -851,20 +785,24 @@ class TestServerTestCase(base.TestCase): # wrong file name mock_exists.side_effect = [True] if distro == consts.UBUNTU: - rv = self.ubuntu_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla') + rv = self.ubuntu_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.bla') elif distro == consts.CENTOS: - rv = self.centos_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla') + rv = self.centos_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.bla') self.assertEqual(400, rv.status_code) mock_exists.side_effect = [True] if distro == consts.UBUNTU: - rv = self.ubuntu_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + rv = self.ubuntu_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.pem') elif distro == consts.CENTOS: - rv = self.centos_app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + rv = self.centos_app.delete( + '/' + api_server.VERSION + + '/loadbalancer/123/certificates/test.pem') self.assertEqual(200, rv.status_code) self.assertEqual(OK, jsonutils.loads(rv.data.decode('utf-8'))) mock_remove.assert_called_once_with( @@ -886,10 +824,10 @@ class TestServerTestCase(base.TestCase): if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + '/loadbalancer/123/certificates/test.pem') elif distro == consts.CENTOS: rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + '/loadbalancer/123/certificates/test.pem') self.assertEqual(404, rv.status_code) self.assertEqual(dict( details='No certificate with filename: test.pem', @@ -901,29 +839,29 @@ class TestServerTestCase(base.TestCase): mock_exists.side_effect = [True] if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', + '/loadbalancer/123/certificates/test.bla', data='TestTest') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', + '/loadbalancer/123/certificates/test.bla', data='TestTest') self.assertEqual(400, rv.status_code) mock_exists.return_value = True mock_exists.side_effect = None if distro == consts.UBUNTU: - path = self.ubuntu_test_server._listener._cert_file_path( + path = self.ubuntu_test_server._loadbalancer._cert_file_path( '123', 'test.pem') elif distro == consts.CENTOS: - path = self.centos_test_server._listener._cert_file_path( + path = self.centos_test_server._loadbalancer._cert_file_path( '123', 'test.pem') self.useFixture(test_utils.OpenFixture(path, CONTENT)) if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + '/loadbalancer/123/certificates/test.pem') elif distro == consts.CENTOS: rv = self.centos_app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + '/loadbalancer/123/certificates/test.pem') self.assertEqual(200, rv.status_code) self.assertEqual(dict(md5sum=hashlib.md5(six.b(CONTENT)).hexdigest()), jsonutils.loads(rv.data.decode('utf-8'))) @@ -942,20 +880,20 @@ class TestServerTestCase(base.TestCase): # wrong file name if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', + '/loadbalancer/123/certificates/test.bla', data='TestTest') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', + '/loadbalancer/123/certificates/test.bla', data='TestTest') self.assertEqual(400, rv.status_code) mock_exists.return_value = True if distro == consts.UBUNTU: - path = self.ubuntu_test_server._listener._cert_file_path( + path = self.ubuntu_test_server._loadbalancer._cert_file_path( '123', 'test.pem') elif distro == consts.CENTOS: - path = self.centos_test_server._listener._cert_file_path( + path = self.centos_test_server._loadbalancer._cert_file_path( '123', 'test.pem') m = self.useFixture(test_utils.OpenFixture(path)).mock_open @@ -963,11 +901,11 @@ class TestServerTestCase(base.TestCase): if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/' + '/loadbalancer/123/certificates/' 'test.pem', data='TestTest') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/' + '/loadbalancer/123/certificates/' 'test.pem', data='TestTest') self.assertEqual(200, rv.status_code) self.assertEqual(OK, jsonutils.loads(rv.data.decode('utf-8'))) @@ -980,11 +918,11 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): if distro == consts.UBUNTU: rv = self.ubuntu_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/' + '/loadbalancer/123/certificates/' 'test.pem', data='TestTest') elif distro == consts.CENTOS: rv = self.centos_app.put('/' + api_server.VERSION + - '/listeners/123/certificates/' + '/loadbalancer/123/certificates/' 'test.pem', data='TestTest') self.assertEqual(200, rv.status_code) self.assertEqual(OK, jsonutils.loads(rv.data.decode('utf-8'))) @@ -2626,7 +2564,7 @@ class TestServerTestCase(base.TestCase): haproxy_count = random.randrange(0, 100) mock_count_haproxy.return_value = haproxy_count - expected_dict = {'active': True, 'api_version': '0.5', + expected_dict = {'active': True, 'api_version': '1.0', 'cpu': {'soft_irq': cpu_softirq, 'system': cpu_system, 'total': cpu_total, 'user': cpu_user}, 'disk': {'available': disk_available, @@ -2699,3 +2637,11 @@ class TestServerTestCase(base.TestCase): rv = self.centos_app.put('/' + api_server.VERSION + '/config', data='TestTest') self.assertEqual(500, rv.status_code) + + def test_version_discovery(self): + self.test_client = server.Server().app.test_client() + expected_dict = {'api_version': api_server.VERSION} + rv = self.test_client.get('/') + self.assertEqual(200, rv.status_code) + self.assertEqual(expected_dict, + jsonutils.loads(rv.data.decode('utf-8'))) diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 7cc38b50de..6071818e20 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -3274,7 +3274,8 @@ class AmphoraRepositoryTest(BaseRepositoryTest): enabled=True, peer_port=1025, default_pool_id=pool.id) listener_ref = {listener.id: {'operating_status': constants.ONLINE, - 'protocol': constants.PROTOCOL_HTTP}} + 'protocol': constants.PROTOCOL_HTTP, + 'enabled': 1}} lb_ref['listeners'] = listener_ref # Test with an LB, pool, and listener (no members) diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py index f74dbf6594..8eacfc429d 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py @@ -19,13 +19,18 @@ from oslo_utils import uuidutils from octavia.amphorae.backends.agent import api_server from octavia.amphorae.backends.agent.api_server import amphora_info +from octavia.amphorae.backends.agent.api_server import util +from octavia.common.jinja.haproxy.combined_listeners import jinja_cfg from octavia.tests.common import utils as test_utils import octavia.tests.unit.base as base +from octavia.tests.unit.common.sample_configs import sample_configs_combined class TestAmphoraInfo(base.TestCase): API_VERSION = random.randrange(0, 10000) + BASE_AMP_PATH = '/var/lib/octavia' + BASE_CRT_PATH = BASE_AMP_PATH + '/certs' HAPROXY_VERSION = random.randrange(0, 10000) KEEPALIVED_VERSION = random.randrange(0, 10000) IPVSADM_VERSION = random.randrange(0, 10000) @@ -33,6 +38,7 @@ class TestAmphoraInfo(base.TestCase): FAKE_LISTENER_ID_2 = uuidutils.generate_uuid() FAKE_LISTENER_ID_3 = uuidutils.generate_uuid() FAKE_LISTENER_ID_4 = uuidutils.generate_uuid() + LB_ID_1 = uuidutils.generate_uuid() def setUp(self): super(TestAmphoraInfo, self).setUp() @@ -40,6 +46,23 @@ class TestAmphoraInfo(base.TestCase): self.amp_info = amphora_info.AmphoraInfo(self.osutils_mock) self.udp_driver = mock.MagicMock() + # setup a fake haproxy config file + templater = jinja_cfg.JinjaTemplater( + base_amp_path=self.BASE_AMP_PATH, + base_crt_dir=self.BASE_CRT_PATH) + tls_tupel = sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='imaCert1', private_key='imaPrivateKey1', + primary_cn='FakeCN') + self.rendered_haproxy_cfg = templater.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, sni=True)], + tls_tupel) + path = util.config_path(self.LB_ID_1) + self.useFixture(test_utils.OpenFixture(path, + self.rendered_haproxy_cfg)) + def _return_version(self, package_name): if package_name == 'ipvsadm': return self.IPVSADM_VERSION @@ -138,8 +161,8 @@ class TestAmphoraInfo(base.TestCase): u'haproxy_count': 5, u'haproxy_version': self.HAPROXY_VERSION, u'hostname': u'FAKE_HOST', - u'listeners': [self.FAKE_LISTENER_ID_1, - self.FAKE_LISTENER_ID_2], + u'listeners': sorted([self.FAKE_LISTENER_ID_1, + self.FAKE_LISTENER_ID_2]), u'load': [u'0.09', u'0.11', u'0.10'], u'memory': {u'buffers': 344792, u'cached': 4271856, @@ -163,8 +186,7 @@ class TestAmphoraInfo(base.TestCase): 'get_udp_listeners', return_value=[FAKE_LISTENER_ID_3, FAKE_LISTENER_ID_4]) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listeners', return_value=[FAKE_LISTENER_ID_1, - FAKE_LISTENER_ID_2]) + 'get_loadbalancers') @mock.patch('octavia.amphorae.backends.agent.api_server.' 'amphora_info.AmphoraInfo._get_meminfo') @mock.patch('octavia.amphorae.backends.agent.api_server.' @@ -182,7 +204,7 @@ class TestAmphoraInfo(base.TestCase): def test_compile_amphora_details_for_udp(self, mhostname, m_count, m_pkg_version, m_load, m_get_nets, m_os, m_cpu, mget_mem, - mget_listener, mget_udp_listener): + mock_get_lb, mget_udp_listener): mget_mem.return_value = {'SwapCached': 0, 'Buffers': 344792, 'MemTotal': 21692784, 'Cached': 4271856, 'Slab': 534384, 'MemFree': 12685624, @@ -205,6 +227,7 @@ class TestAmphoraInfo(base.TestCase): self.udp_driver.get_subscribed_amp_compile_info.return_value = [ 'keepalived', 'ipvsadm'] self.udp_driver.is_listener_running.side_effect = [True, False] + mock_get_lb.return_value = [self.LB_ID_1] original_version = api_server.VERSION api_server.VERSION = self.API_VERSION expected_dict = {u'active': True, @@ -221,10 +244,10 @@ class TestAmphoraInfo(base.TestCase): u'ipvsadm_version': self.IPVSADM_VERSION, u'udp_listener_process_count': 1, u'hostname': u'FAKE_HOST', - u'listeners': list(set([self.FAKE_LISTENER_ID_1, - self.FAKE_LISTENER_ID_2, - self.FAKE_LISTENER_ID_3, - self.FAKE_LISTENER_ID_4])), + u'listeners': sorted(list(set( + [self.FAKE_LISTENER_ID_3, + self.FAKE_LISTENER_ID_4, + 'sample_listener_id_1']))), u'load': [u'0.09', u'0.11', u'0.10'], u'memory': {u'buffers': 344792, u'cached': 4271856, @@ -245,7 +268,7 @@ class TestAmphoraInfo(base.TestCase): api_server.VERSION = original_version @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'is_listener_running') + 'is_lb_running') def test__count_haproxy_process(self, mock_is_running): # Test no listeners passed in diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_haproxy_compatibility.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_haproxy_compatibility.py index 3696e37b4b..a3ac074814 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_haproxy_compatibility.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_haproxy_compatibility.py @@ -17,7 +17,7 @@ import mock from octavia.amphorae.backends.agent.api_server import haproxy_compatibility from octavia.common import constants import octavia.tests.unit.base as base -from octavia.tests.unit.common.sample_configs import sample_configs +from octavia.tests.unit.common.sample_configs import sample_configs_combined class HAProxyCompatTestCase(base.TestCase): @@ -30,7 +30,7 @@ class HAProxyCompatTestCase(base.TestCase): " user nobody\n" " log /dev/log local0\n" " log /dev/log local1 notice\n" - " stats socket /var/lib/octavia/sample_listener_id_1.sock" + " stats socket /var/lib/octavia/sample_loadbalancer_id_1.sock" " mode 0666 level user\n" " maxconn {maxconn}\n\n" "defaults\n" @@ -47,11 +47,11 @@ class HAProxyCompatTestCase(base.TestCase): " maxconn {maxconn}\n" " bind 10.0.0.2:80\n" " mode http\n" - " default_backend sample_pool_id_1\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" " timeout client 50000\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) self.backend_without_external = ( - "backend sample_pool_id_1\n" + "backend sample_pool_id_1:sample_listener_id_1\n" " mode http\n" " balance roundrobin\n" " cookie SRV insert indirect nocache\n" @@ -69,7 +69,7 @@ class HAProxyCompatTestCase(base.TestCase): "sample_member_id_2\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) self.backend_with_external = ( - "backend sample_pool_id_1\n" + "backend sample_pool_id_1:sample_listener_id_1\n" " mode http\n" " balance roundrobin\n" " cookie SRV insert indirect nocache\n" @@ -103,7 +103,7 @@ class HAProxyCompatTestCase(base.TestCase): def test_process_cfg_for_version_compat(self, mock_get_version): # Test 1.6 version path, no change to config expected mock_get_version.return_value = [1, 6] - test_config = sample_configs.sample_base_expected_config( + test_config = sample_configs_combined.sample_base_expected_config( backend=self.backend_with_external) result_config = haproxy_compatibility.process_cfg_for_version_compat( test_config) @@ -111,7 +111,7 @@ class HAProxyCompatTestCase(base.TestCase): # Test 1.5 version path, external-check should be removed mock_get_version.return_value = [1, 5] - test_config = sample_configs.sample_base_expected_config( + test_config = sample_configs_combined.sample_base_expected_config( backend=self.backend_with_external) result_config = haproxy_compatibility.process_cfg_for_version_compat( test_config) diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py index d180d9d1f9..bbe03a73c3 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py @@ -13,7 +13,6 @@ # under the License. import mock -from werkzeug import exceptions from oslo_utils import uuidutils @@ -29,13 +28,6 @@ class KeepalivedLvsTestCase(base.TestCase): super(KeepalivedLvsTestCase, self).setUp() self.test_keepalivedlvs = keepalivedlvs.KeepalivedLvs() - @mock.patch('os.path.exists') - def test_get_udp_listener_status_no_exists(self, m_exist): - m_exist.return_value = False - self.assertRaises(exceptions.HTTPException, - self.test_keepalivedlvs.get_udp_listener_status, - self.FAKE_ID) - @mock.patch.object(keepalivedlvs, "webob") @mock.patch('os.path.exists') def test_delete_udp_listener_not_exist(self, m_exist, m_webob): diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_listener.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_loadbalancer.py similarity index 56% rename from octavia/tests/unit/amphorae/backends/agent/api_server/test_listener.py rename to octavia/tests/unit/amphorae/backends/agent/api_server/test_loadbalancer.py index 15a006acc4..fb23082711 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_listener.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_loadbalancer.py @@ -17,141 +17,35 @@ import subprocess import mock from oslo_utils import uuidutils -from octavia.amphorae.backends.agent.api_server import listener +from octavia.amphorae.backends.agent.api_server import loadbalancer from octavia.amphorae.backends.agent.api_server import util as agent_util from octavia.common import constants as consts -from octavia.common.jinja.haproxy import jinja_cfg from octavia.tests.common import utils as test_utils import octavia.tests.unit.base as base -from octavia.tests.unit.common.sample_configs import sample_configs -BASE_AMP_PATH = '/var/lib/octavia' -BASE_CRT_PATH = BASE_AMP_PATH + '/certs' LISTENER_ID1 = uuidutils.generate_uuid() +LB_ID1 = uuidutils.generate_uuid() class ListenerTestCase(base.TestCase): def setUp(self): super(ListenerTestCase, self).setUp() - self.jinja_cfg = jinja_cfg.JinjaTemplater( - base_amp_path=BASE_AMP_PATH, - base_crt_dir=BASE_CRT_PATH) self.mock_platform = mock.patch("distro.id").start() self.mock_platform.return_value = "ubuntu" - self.test_listener = listener.Listener() - - def test_parse_haproxy_config(self): - # template_tls - tls_tupe = sample_configs.sample_tls_container_tuple( - id='tls_container_id', - certificate='imaCert1', private_key='imaPrivateKey1', - primary_cn='FakeCN') - rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='TERMINATED_HTTPS', - tls=True, sni=True), - tls_tupe) - - path = agent_util.config_path(LISTENER_ID1) - self.useFixture(test_utils.OpenFixture(path, rendered_obj)) - - res = self.test_listener._parse_haproxy_file(LISTENER_ID1) - self.assertEqual('TERMINATED_HTTPS', res['mode']) - self.assertEqual('/var/lib/octavia/sample_listener_id_1.sock', - res['stats_socket']) - self.assertEqual( - '/var/lib/octavia/certs/sample_listener_id_1/tls_container_id.pem', - res['ssl_crt']) - - # render_template_tls_no_sni - rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( - proto='TERMINATED_HTTPS', tls=True), - tls_cert=sample_configs.sample_tls_container_tuple( - id='tls_container_id', - certificate='ImAalsdkfjCert', - private_key='ImAsdlfksdjPrivateKey', - primary_cn="FakeCN")) - - self.useFixture(test_utils.OpenFixture(path, rendered_obj)) - - res = self.test_listener._parse_haproxy_file(LISTENER_ID1) - self.assertEqual('TERMINATED_HTTPS', res['mode']) - self.assertEqual(BASE_AMP_PATH + '/sample_listener_id_1.sock', - res['stats_socket']) - self.assertEqual( - BASE_CRT_PATH + '/sample_listener_id_1/tls_container_id.pem', - res['ssl_crt']) - - # render_template_http - rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple()) - - self.useFixture(test_utils.OpenFixture(path, rendered_obj)) - - res = self.test_listener._parse_haproxy_file(LISTENER_ID1) - self.assertEqual('HTTP', res['mode']) - self.assertEqual(BASE_AMP_PATH + '/sample_listener_id_1.sock', - res['stats_socket']) - self.assertIsNone(res['ssl_crt']) - - # template_https - rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS')) - self.useFixture(test_utils.OpenFixture(path, rendered_obj)) - - res = self.test_listener._parse_haproxy_file(LISTENER_ID1) - self.assertEqual('TCP', res['mode']) - self.assertEqual(BASE_AMP_PATH + '/sample_listener_id_1.sock', - res['stats_socket']) - self.assertIsNone(res['ssl_crt']) - - # Bogus format - self.useFixture(test_utils.OpenFixture(path, 'Bogus')) - try: - res = self.test_listener._parse_haproxy_file(LISTENER_ID1) - self.fail("No Exception?") - except listener.ParsingError: - pass - - @mock.patch('os.path.exists') - @mock.patch('octavia.amphorae.backends.agent.api_server' + - '.util.get_haproxy_pid') - def test_check_listener_status(self, mock_pid, mock_exists): - mock_pid.return_value = '1245' - mock_exists.side_effect = [True, True] - config_path = agent_util.config_path(LISTENER_ID1) - file_contents = 'frontend {}'.format(LISTENER_ID1) - self.useFixture(test_utils.OpenFixture(config_path, file_contents)) - self.assertEqual( - consts.ACTIVE, - self.test_listener._check_listener_status(LISTENER_ID1)) - - mock_exists.side_effect = [True, False] - self.assertEqual( - consts.ERROR, - self.test_listener._check_listener_status(LISTENER_ID1)) - - mock_exists.side_effect = [False] - self.assertEqual( - consts.OFFLINE, - self.test_listener._check_listener_status(LISTENER_ID1)) + self.test_loadbalancer = loadbalancer.Loadbalancer() @mock.patch('os.makedirs') @mock.patch('os.path.exists') @mock.patch('os.listdir') @mock.patch('os.path.join') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' - 'get_listeners') + 'get_loadbalancers') @mock.patch('octavia.amphorae.backends.agent.api_server.util' '.haproxy_sock_path') - def test_vrrp_check_script_update(self, mock_sock_path, mock_get_listeners, + def test_vrrp_check_script_update(self, mock_sock_path, mock_get_lbs, mock_join, mock_listdir, mock_exists, mock_makedirs): - mock_get_listeners.return_value = ['abc', LISTENER_ID1] + mock_get_lbs.return_value = ['abc', LB_ID1] mock_sock_path.return_value = 'listener.sock' mock_exists.return_value = False cmd = 'haproxy-vrrp-check ' + ' '.join(['listener.sock']) + '; exit $?' @@ -159,17 +53,17 @@ class ListenerTestCase(base.TestCase): path = agent_util.keepalived_dir() m = self.useFixture(test_utils.OpenFixture(path)).mock_open - self.test_listener.vrrp_check_script_update(LISTENER_ID1, 'stop') + self.test_loadbalancer.vrrp_check_script_update(LB_ID1, 'stop') handle = m() handle.write.assert_called_once_with(cmd) - mock_get_listeners.return_value = ['abc', LISTENER_ID1] + mock_get_lbs.return_value = ['abc', LB_ID1] cmd = ('haproxy-vrrp-check ' + ' '.join(['listener.sock', 'listener.sock']) + '; exit ' '$?') m = self.useFixture(test_utils.OpenFixture(path)).mock_open - self.test_listener.vrrp_check_script_update(LISTENER_ID1, 'start') + self.test_loadbalancer.vrrp_check_script_update(LB_ID1, 'start') handle = m() handle.write.assert_called_once_with(cmd) @@ -181,29 +75,29 @@ class ListenerTestCase(base.TestCase): mock_exists.side_effect = [True, True] self.assertEqual( consts.ACTIVE, - self.test_listener._check_haproxy_status(LISTENER_ID1)) + self.test_loadbalancer._check_haproxy_status(LISTENER_ID1)) mock_exists.side_effect = [True, False] self.assertEqual( consts.OFFLINE, - self.test_listener._check_haproxy_status(LISTENER_ID1)) + self.test_loadbalancer._check_haproxy_status(LISTENER_ID1)) mock_exists.side_effect = [False] self.assertEqual( consts.OFFLINE, - self.test_listener._check_haproxy_status(LISTENER_ID1)) + self.test_loadbalancer._check_haproxy_status(LISTENER_ID1)) - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_check_haproxy_status') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - 'vrrp_check_script_update') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer._check_haproxy_status') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer.vrrp_check_script_update') @mock.patch('os.path.exists') - @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' - '_check_listener_exists') + @mock.patch('octavia.amphorae.backends.agent.api_server.loadbalancer.' + 'Loadbalancer._check_lb_exists') @mock.patch('subprocess.check_output') - def test_start_stop_listener(self, mock_check_output, mock_list_exists, - mock_path_exists, mock_vrrp_update, - mock_check_status): + def test_start_stop_lb(self, mock_check_output, mock_lb_exists, + mock_path_exists, mock_vrrp_update, + mock_check_status): listener_id = uuidutils.generate_uuid() mock_path_exists.side_effect = [False, True, True, False, False] @@ -214,12 +108,12 @@ class ListenerTestCase(base.TestCase): ref_command_split.append('haproxy-{}'.format(listener_id)) ref_command_split.append(consts.AMP_ACTION_START) - result = self.test_listener.start_stop_listener( + result = self.test_loadbalancer.start_stop_lb( listener_id, consts.AMP_ACTION_START) mock_check_output.assert_called_once_with(ref_command_split, stderr=subprocess.STDOUT) - mock_list_exists.assert_called_once_with(listener_id) + mock_lb_exists.assert_called_once_with(listener_id) mock_vrrp_update.assert_not_called() self.assertEqual(202, result.status_code) self.assertEqual('OK', result.json['message']) @@ -228,7 +122,7 @@ class ListenerTestCase(base.TestCase): self.assertEqual(ref_details, result.json['details']) # Happy path - VRRP - RELOAD - mock_list_exists.reset_mock() + mock_lb_exists.reset_mock() mock_vrrp_update.reset_mock() mock_check_output.reset_mock() @@ -236,12 +130,12 @@ class ListenerTestCase(base.TestCase): ref_command_split.append('haproxy-{}'.format(listener_id)) ref_command_split.append(consts.AMP_ACTION_RELOAD) - result = self.test_listener.start_stop_listener( + result = self.test_loadbalancer.start_stop_lb( listener_id, consts.AMP_ACTION_RELOAD) mock_check_output.assert_called_once_with(ref_command_split, stderr=subprocess.STDOUT) - mock_list_exists.assert_called_once_with(listener_id) + mock_lb_exists.assert_called_once_with(listener_id) mock_vrrp_update.assert_called_once_with(listener_id, consts.AMP_ACTION_RELOAD) self.assertEqual(202, result.status_code) @@ -251,7 +145,7 @@ class ListenerTestCase(base.TestCase): self.assertEqual(ref_details, result.json['details']) # Happy path - VRRP - RELOAD - OFFLINE - mock_list_exists.reset_mock() + mock_lb_exists.reset_mock() mock_vrrp_update.reset_mock() mock_check_output.reset_mock() @@ -259,12 +153,12 @@ class ListenerTestCase(base.TestCase): ref_command_split.append('haproxy-{}'.format(listener_id)) ref_command_split.append(consts.AMP_ACTION_START) - result = self.test_listener.start_stop_listener( + result = self.test_loadbalancer.start_stop_lb( listener_id, consts.AMP_ACTION_RELOAD) mock_check_output.assert_called_once_with(ref_command_split, stderr=subprocess.STDOUT) - mock_list_exists.assert_called_once_with(listener_id) + mock_lb_exists.assert_called_once_with(listener_id) mock_vrrp_update.assert_called_once_with(listener_id, consts.AMP_ACTION_RELOAD) self.assertEqual(202, result.status_code) @@ -274,7 +168,7 @@ class ListenerTestCase(base.TestCase): self.assertEqual(ref_details, result.json['details']) # Unhappy path - Not already running - mock_list_exists.reset_mock() + mock_lb_exists.reset_mock() mock_vrrp_update.reset_mock() mock_check_output.reset_mock() @@ -285,12 +179,12 @@ class ListenerTestCase(base.TestCase): mock_check_output.side_effect = subprocess.CalledProcessError( output=b'bogus', returncode=-2, cmd='sit') - result = self.test_listener.start_stop_listener( + result = self.test_loadbalancer.start_stop_lb( listener_id, consts.AMP_ACTION_START) mock_check_output.assert_called_once_with(ref_command_split, stderr=subprocess.STDOUT) - mock_list_exists.assert_called_once_with(listener_id) + mock_lb_exists.assert_called_once_with(listener_id) mock_vrrp_update.assert_not_called() self.assertEqual(500, result.status_code) self.assertEqual('Error {}ing haproxy'.format(consts.AMP_ACTION_START), @@ -298,7 +192,7 @@ class ListenerTestCase(base.TestCase): self.assertEqual('bogus', result.json['details']) # Unhappy path - Already running - mock_list_exists.reset_mock() + mock_lb_exists.reset_mock() mock_vrrp_update.reset_mock() mock_check_output.reset_mock() @@ -309,12 +203,12 @@ class ListenerTestCase(base.TestCase): mock_check_output.side_effect = subprocess.CalledProcessError( output=b'Job is already running', returncode=-2, cmd='sit') - result = self.test_listener.start_stop_listener( + result = self.test_loadbalancer.start_stop_lb( listener_id, consts.AMP_ACTION_START) mock_check_output.assert_called_once_with(ref_command_split, stderr=subprocess.STDOUT) - mock_list_exists.assert_called_once_with(listener_id) + mock_lb_exists.assert_called_once_with(listener_id) mock_vrrp_update.assert_not_called() self.assertEqual(202, result.status_code) self.assertEqual('OK', result.json['message']) @@ -324,14 +218,62 @@ class ListenerTestCase(base.TestCase): # Invalid action mock_check_output.reset_mock() - mock_list_exists.reset_mock() + mock_lb_exists.reset_mock() mock_path_exists.reset_mock() mock_vrrp_update.reset_mock() - result = self.test_listener.start_stop_listener(listener_id, 'bogus') + result = self.test_loadbalancer.start_stop_lb(listener_id, 'bogus') self.assertEqual(400, result.status_code) self.assertEqual('Invalid Request', result.json['message']) self.assertEqual('Unknown action: bogus', result.json['details']) - mock_list_exists.assert_not_called() + mock_lb_exists.assert_not_called() mock_path_exists.assert_not_called() mock_vrrp_update.assert_not_called() mock_check_output.assert_not_called() + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'config_path') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_haproxy_pid') + @mock.patch('os.path.exists') + def test_get_listeners_on_lb(self, mock_exists, mock_get_haproxy_pid, + mock_config_path): + + fake_cfg_path = '/some/fake/cfg/file.cfg' + mock_config_path.return_value = fake_cfg_path + mock_get_haproxy_pid.return_value = 'fake_pid' + + # Finds two listeners + mock_exists.side_effect = [True, True] + fake_cfg_data = 'frontend list1\nbackend foo\nfrontend list2' + self.useFixture( + test_utils.OpenFixture(fake_cfg_path, fake_cfg_data)).mock_open + result = self.test_loadbalancer._get_listeners_on_lb(LB_ID1) + self.assertEqual(['list1', 'list2'], result) + mock_exists.assert_has_calls([mock.call(agent_util.pid_path(LB_ID1)), + mock.call('/proc/fake_pid')]) + + # No PID file, no listeners + mock_exists.reset_mock() + mock_exists.side_effect = [False] + result = self.test_loadbalancer._get_listeners_on_lb(LB_ID1) + self.assertEqual([], result) + mock_exists.assert_called_once_with(agent_util.pid_path(LB_ID1)) + + # PID file, no running process, no listeners + mock_exists.reset_mock() + mock_exists.side_effect = [True, False] + result = self.test_loadbalancer._get_listeners_on_lb(LB_ID1) + self.assertEqual([], result) + mock_exists.assert_has_calls([mock.call(agent_util.pid_path(LB_ID1)), + mock.call('/proc/fake_pid')]) + + # PID file, running process, no listeners + mock_exists.reset_mock() + mock_exists.side_effect = [True, True] + fake_cfg_data = 'backend only' + self.useFixture( + test_utils.OpenFixture(fake_cfg_path, fake_cfg_data)).mock_open + result = self.test_loadbalancer._get_listeners_on_lb(LB_ID1) + self.assertEqual([], result) + mock_exists.assert_has_calls([mock.call(agent_util.pid_path(LB_ID1)), + mock.call('/proc/fake_pid')]) diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py index a6903d1535..8854209609 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py @@ -22,11 +22,15 @@ from oslo_utils import uuidutils from octavia.amphorae.backends.agent.api_server import util from octavia.common import constants as consts +from octavia.common.jinja.haproxy.combined_listeners import jinja_cfg from octavia.tests.common import utils as test_utils import octavia.tests.unit.base as base +from octavia.tests.unit.common.sample_configs import sample_configs_combined - +BASE_AMP_PATH = '/var/lib/octavia' +BASE_CRT_PATH = BASE_AMP_PATH + '/certs' CONF = cfg.CONF +LISTENER_ID1 = uuidutils.generate_uuid() class TestUtil(base.TestCase): @@ -34,6 +38,9 @@ class TestUtil(base.TestCase): super(TestUtil, self).setUp() self.CONF = self.useFixture(oslo_fixture.Config(cfg.CONF)) self.listener_id = uuidutils.generate_uuid() + self.jinja_cfg = jinja_cfg.JinjaTemplater( + base_amp_path=BASE_AMP_PATH, + base_crt_dir=BASE_CRT_PATH) def test_keepalived_lvs_dir(self): fake_path = '/fake/path' @@ -171,7 +178,7 @@ class TestUtil(base.TestCase): mock_cfg_path.return_value = '/there' mock_path_exists.side_effect = [True, False, True, False, False] - result = util.get_listener_protocol('1') + result = util.get_protocol_for_lb_object('1') mock_cfg_path.assert_called_once_with('1') mock_path_exists.assert_called_once_with('/there') @@ -180,7 +187,7 @@ class TestUtil(base.TestCase): mock_cfg_path.reset_mock() - result = util.get_listener_protocol('2') + result = util.get_protocol_for_lb_object('2') mock_cfg_path.assert_called_once_with('2') mock_lvs_path.assert_called_once_with('2') @@ -189,8 +196,97 @@ class TestUtil(base.TestCase): mock_cfg_path.reset_mock() mock_lvs_path.reset_mock() - result = util.get_listener_protocol('3') + result = util.get_protocol_for_lb_object('3') mock_cfg_path.assert_called_once_with('3') mock_lvs_path.assert_called_once_with('3') self.assertIsNone(result) + + def test_parse_haproxy_config(self): + # template_tls + tls_tupe = sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='imaCert1', private_key='imaPrivateKey1', + primary_cn='FakeCN') + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, sni=True)], + tls_tupe) + + path = util.config_path(LISTENER_ID1) + self.useFixture(test_utils.OpenFixture(path, rendered_obj)) + + res = util.parse_haproxy_file(LISTENER_ID1) + listener_dict = res[1]['sample_listener_id_1'] + self.assertEqual('TERMINATED_HTTPS', listener_dict['mode']) + self.assertEqual('/var/lib/octavia/sample_loadbalancer_id_1.sock', + res[0]) + self.assertEqual( + '/var/lib/octavia/certs/sample_loadbalancer_id_1/' + 'tls_container_id.pem crt /var/lib/octavia/certs/' + 'sample_loadbalancer_id_1', + listener_dict['ssl_crt']) + + # render_template_tls_no_sni + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True)], + tls_cert=sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='ImAalsdkfjCert', + private_key='ImAsdlfksdjPrivateKey', + primary_cn="FakeCN")) + + self.useFixture(test_utils.OpenFixture(path, rendered_obj)) + + res = util.parse_haproxy_file(LISTENER_ID1) + listener_dict = res[1]['sample_listener_id_1'] + self.assertEqual('TERMINATED_HTTPS', listener_dict['mode']) + self.assertEqual(BASE_AMP_PATH + '/sample_loadbalancer_id_1.sock', + res[0]) + self.assertEqual( + BASE_CRT_PATH + '/sample_loadbalancer_id_1/tls_container_id.pem', + listener_dict['ssl_crt']) + + # render_template_http + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple()]) + + self.useFixture(test_utils.OpenFixture(path, rendered_obj)) + + res = util.parse_haproxy_file(LISTENER_ID1) + listener_dict = res[1]['sample_listener_id_1'] + self.assertEqual('HTTP', listener_dict['mode']) + self.assertEqual(BASE_AMP_PATH + '/sample_loadbalancer_id_1.sock', + res[0]) + self.assertIsNone(listener_dict.get('ssl_crt', None)) + + # template_https + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(proto='HTTPS')]) + self.useFixture(test_utils.OpenFixture(path, rendered_obj)) + + res = util.parse_haproxy_file(LISTENER_ID1) + listener_dict = res[1]['sample_listener_id_1'] + self.assertEqual('TCP', listener_dict['mode']) + self.assertEqual(BASE_AMP_PATH + '/sample_loadbalancer_id_1.sock', + res[0]) + self.assertIsNone(listener_dict.get('ssl_crt', None)) + + # Bogus format + self.useFixture(test_utils.OpenFixture(path, 'Bogus')) + try: + res = util.parse_haproxy_file(LISTENER_ID1) + self.fail("No Exception?") + except util.ParsingError: + pass + + # Bad listener mode + fake_cfg = 'stats socket foo\nfrontend {}\nmode\n'.format(LISTENER_ID1) + self.useFixture(test_utils.OpenFixture(path, fake_cfg)) + self.assertRaises(util.ParsingError, util.parse_haproxy_file, + LISTENER_ID1) diff --git a/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py b/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py index 7d225f799d..10a74b977d 100644 --- a/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py +++ b/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py @@ -52,7 +52,7 @@ SAMPLE_STATS = ({'': '', 'status': 'OPEN', 'lastchg': '', 'rate': '0', 'req_rate': '0', 'check_status': '', 'econ': '', 'comp_out': '0', 'wredis': '', 'dresp': '0', 'ereq': '0', 'tracked': '', 'comp_in': '0', - 'pxname': '490b6ae7-21aa-43f1-b82a-68ddcd2ca2fb', + 'pxname': LISTENER_ID1, 'dreq': '0', 'hrsp_5xx': '0', 'last_chk': '', 'check_code': '', 'sid': '0', 'bout': '0', 'hrsp_1xx': '0', 'qlimit': '', 'hrsp_other': '0', 'bin': '0', 'rtime': '', @@ -107,25 +107,16 @@ SAMPLE_STATS = ({'': '', 'status': 'OPEN', 'lastchg': '', SAMPLE_STATS_MSG = { 'listeners': { LISTENER_ID1: { - 'pools': { - '432fc8b3-d446-48d4-bb64-13beb90e22bc': { - 'members': { - '302e33d9-dee1-4de9-98d5-36329a06fb58': 'DOWN'}, - 'status': 'UP'}}, 'stats': { 'totconns': 0, 'conns': 0, 'tx': 0, 'rx': 0, 'ereq': 0}, 'status': 'OPEN'}, - LISTENER_ID2: { - 'pools': { - '432fc8b3-d446-48d4-bb64-13beb90e22bc': { - 'members': { - '302e33d9-dee1-4de9-98d5-36329a06fb58': 'DOWN'}, - 'status': 'UP'}}, - 'stats': { - 'totconns': 0, 'conns': 0, - 'tx': 0, 'rx': 0, 'ereq': 0}, - 'status': 'OPEN'} + }, + 'pools': {'432fc8b3-d446-48d4-bb64-13beb90e22bc': { + 'members': {'302e33d9-dee1-4de9-98d5-36329a06fb58': 'DOWN'}, + 'status': 'UP'}, '432fc8b3-d446-48d4-bb64-13beb90e22bc': { + 'members': {'302e33d9-dee1-4de9-98d5-36329a06fb58': 'DOWN'}, + 'status': 'UP'}, }, 'id': None, 'seq': 0, @@ -141,7 +132,7 @@ class TestHealthDaemon(base.TestCase): conf.config(group="haproxy_amphora", base_path=BASE_PATH) @mock.patch('octavia.amphorae.backends.agent.' - 'api_server.util.get_listeners') + 'api_server.util.get_loadbalancers') def test_list_sock_stat_files(self, mock_get_listener): mock_get_listener.return_value = LISTENER_IDS @@ -297,7 +288,7 @@ class TestHealthDaemon(base.TestCase): stats_query_mock.get_pool_status.assert_called_once_with() @mock.patch('octavia.amphorae.backends.agent.api_server.' - 'util.is_listener_running') + 'util.is_lb_running') @mock.patch('octavia.amphorae.backends.health_daemon.' 'health_daemon.get_stats') @mock.patch('octavia.amphorae.backends.health_daemon.' @@ -318,7 +309,7 @@ class TestHealthDaemon(base.TestCase): mock_get_stats.assert_any_call('TEST2') @mock.patch('octavia.amphorae.backends.agent.api_server.' - 'util.is_listener_running') + 'util.is_lb_running') @mock.patch('octavia.amphorae.backends.health_daemon.' 'health_daemon.get_stats') @mock.patch('octavia.amphorae.backends.health_daemon.' @@ -336,25 +327,6 @@ class TestHealthDaemon(base.TestCase): self.assertEqual(1, mock_get_stats.call_count) - @mock.patch('octavia.amphorae.backends.agent.api_server.' - 'util.is_listener_running') - @mock.patch('octavia.amphorae.backends.health_daemon.' - 'health_daemon.get_stats') - @mock.patch('octavia.amphorae.backends.health_daemon.' - 'health_daemon.list_sock_stat_files') - def test_build_stats_message_mismatch_pool(self, mock_list_files, - mock_get_stats, - mock_is_running): - mock_list_files.return_value = {LISTENER_ID1: 'TEST', - LISTENER_ID2: 'TEST2'} - - mock_is_running.return_value = True - mock_get_stats.return_value = SAMPLE_STATS, SAMPLE_BOGUS_POOL_STATUS - - msg = health_daemon.build_stats_message() - - self.assertEqual({}, msg['listeners'][LISTENER_ID1]['pools']) - @mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query." "get_udp_listener_pool_status") @mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query." @@ -411,7 +383,7 @@ class TestHealthDaemon(base.TestCase): 'status': constants.DOWN, 'pools': {}, 'stats': {'conns': 0, 'totconns': 0, 'ereq': 0, - 'rx': 0, 'tx': 0}}}, 'id': None, + 'rx': 0, 'tx': 0}}}, 'pools': {}, 'id': None, 'seq': mock.ANY, 'ver': health_daemon.MSG_VER} msg = health_daemon.build_stats_message() self.assertEqual(expected, msg) diff --git a/octavia/tests/unit/amphorae/backends/utils/test_haproxy_query.py b/octavia/tests/unit/amphorae/backends/utils/test_haproxy_query.py index 3d2457a47a..0936fed865 100644 --- a/octavia/tests/unit/amphorae/backends/utils/test_haproxy_query.py +++ b/octavia/tests/unit/amphorae/backends/utils/test_haproxy_query.py @@ -29,22 +29,29 @@ STATS_SOCKET_SAMPLE = ( "_3xx,hrsp_4xx,hrsp_5xx,hrsp_other,hanafail,req_rate,req_rate_max,req_tot" ",cli_abrt,srv_abrt,comp_in,comp_out,comp_byp,comp_rsp,lastsess,last_chk," "last_agt,qtime,ctime,rtime,ttime,\n" - "http-servers,id-34821,0,0,0,0,,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,1,1,575,575" - ",,1,3,1,,0,,2,0,,0,L4TOUT,,30001,0,0,0,0,0,0,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "http-servers,id-34824,0,0,0,0,,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,1,1,567,567," - ",1,3,2,,0,,2,0,,0,L4TOUT,,30001,0,0,0,0,0,0,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "http-servers,BACKEND,0,0,0,0,200,0,0,0,0,0,,0,0,0,0,DOWN,0,0,0,,1,567,567" - ",,1,3,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,0,0,0,0,-1,,,0,0,0,0,\n" - "tcp-servers,id-34833,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,1,1,560,560,," - "1,5,1,,0,,2,0,,0,L4TOUT,,30000,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "tcp-servers,id-34836,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,1,1,552,552,," - "1,5,2,,0,,2,0,,0,L4TOUT,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "tcp-servers,id-34839,0,0,0,0,,0,0,0,,0,,0,0,0,0,DRAIN,0,1,0,0,0,552,0,," - "1,5,2,,0,,2,0,,0,L7OK,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "tcp-servers,id-34842,0,0,0,0,,0,0,0,,0,,0,0,0,0,MAINT,0,1,0,0,0,552,0,," - "1,5,2,,0,,2,0,,0,L7OK,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0,\n" - "tcp-servers,BACKEND,0,0,0,0,200,0,0,0,0,0,,0,0,0,0,UP,0,0,0,,1,552,552" - ",,1,5,0,,0,,1,0,,0,,,,,,,,,,,,,,0,0,0,0,0,0,-1,,,0,0,0,0," + "http-servers:listener-id,id-34821,0,0,0,0,,0,0,0,,0,,0,0,0,0,DOWN,1,1,0," + "1,1,575,575,,1,3,1,,0,,2,0,,0,L4TOUT,,30001,0,0,0,0,0,0,0,,,,0,0,,,,,-1,," + ",0,0,0,0,\n" + "http-servers:listener-id,id-34824,0,0,0,0,,0,0,0,,0,,0,0,0,0,DOWN,1,1,0," + "1,1,567,567,,1,3,2,,0,,2,0,,0,L4TOUT,,30001,0,0,0,0,0,0,0,,,,0,0,,,,,-1,," + ",0,0,0,0,\n" + "http-servers:listener-id,BACKEND,0,0,0,0,200,0,0,0,0,0,,0,0,0,0,DOWN,0,0," + "0,,1,567,567,,1,3,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,0,0,0,0,-1,,,0,0,0," + "0,\n" + "tcp-servers:listener-id,id-34833,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,1,1," + "560,560,,1,5,1,,0,,2,0,,0,L4TOUT,,30000,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0," + "\n" + "tcp-servers:listener-id,id-34836,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,1,1," + "552,552,,1,5,2,,0,,2,0,,0,L4TOUT,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0," + "\n" + "tcp-servers:listener-id,id-34839,0,0,0,0,,0,0,0,,0,,0,0,0,0,DRAIN,0,1,0," + "0,0,552,0,,1,5,2,,0,,2,0,,0,L7OK,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0," + "\n" + "tcp-servers:listener-id,id-34842,0,0,0,0,,0,0,0,,0,,0,0,0,0,MAINT,0,1,0," + "0,0,552,0,,1,5,2,,0,,2,0,,0,L7OK,,30001,,,,,,,0,,,,0,0,,,,,-1,,,0,0,0,0," + "\n" + "tcp-servers:listener-id,BACKEND,0,0,0,0,200,0,0,0,0,0,,0,0,0,0,UP,0,0,0,," + "1,552,552,,1,5,0,,0,,1,0,,0,,,,,,,,,,,,,,0,0,0,0,0,0,-1,,,0,0,0,0," ) INFO_SOCKET_SAMPLE = ( @@ -94,17 +101,19 @@ class QueryTestCase(base.TestCase): self.q._query = query_mock query_mock.return_value = STATS_SOCKET_SAMPLE self.assertEqual( - {'tcp-servers': { + {'tcp-servers:listener-id': { 'status': constants.UP, - 'uuid': 'tcp-servers', + 'listener_uuid': 'listener-id', + 'pool_uuid': 'tcp-servers', 'members': {'id-34833': constants.UP, 'id-34836': constants.UP, 'id-34839': constants.DRAIN, 'id-34842': constants.MAINT}}, - 'http-servers': { + 'http-servers:listener-id': { 'status': constants.DOWN, - 'uuid': 'http-servers', + 'listener_uuid': 'listener-id', + 'pool_uuid': 'http-servers', 'members': {'id-34821': constants.DOWN, 'id-34824': constants.DOWN}}}, diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py similarity index 69% rename from octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py rename to octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py index b09af8558c..6635545a8a 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py @@ -31,8 +31,9 @@ from octavia.db import models from octavia.network import data_models as network_models from octavia.tests.unit import base from octavia.tests.unit.common.sample_configs import sample_certs -from octavia.tests.unit.common.sample_configs import sample_configs +from octavia.tests.unit.common.sample_configs import sample_configs_split +API_VERSION = '0.5' FAKE_CIDR = '198.51.100.0/24' FAKE_GATEWAY = '192.51.100.1' FAKE_IP = '192.0.2.10' @@ -60,24 +61,35 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.cert_manager = mock.MagicMock() self.driver.cert_parser = mock.MagicMock() - self.driver.client = mock.MagicMock() - self.driver.jinja = mock.MagicMock() + self.driver.clients = { + 'base': mock.MagicMock(), + API_VERSION: mock.MagicMock()} + self.driver.clients['base'].get_api_version.return_value = { + 'api_version': API_VERSION} + self.driver.clients[ + API_VERSION].get_info.return_value = { + 'haproxy_version': u'1.6.3-1ubuntu0.1', + 'api_version': API_VERSION} + self.driver.jinja_split = mock.MagicMock() self.driver.udp_jinja = mock.MagicMock() # Build sample Listener and VIP configs - self.sl = sample_configs.sample_listener_tuple( - tls=True, sni=True, client_ca_cert=True, client_crl_cert=True) - self.sl_udp = sample_configs.sample_listener_tuple( + self.sl = sample_configs_split.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + self.sl_udp = sample_configs_split.sample_listener_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, persistence_granularity='255.255.0.0', monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT) - self.pool_has_cert = sample_configs.sample_pool_tuple( + self.pool_has_cert = sample_configs_split.sample_pool_tuple( pool_cert=True, pool_ca_cert=True, pool_crl=True) self.amp = self.sl.load_balancer.amphorae[0] - self.sv = sample_configs.sample_vip_tuple() + self.sv = sample_configs_split.sample_vip_tuple() self.lb = self.sl.load_balancer + self.lb_udp = ( + sample_configs_split.sample_lb_with_udp_listener_tuple()) self.fixed_ip = mock.MagicMock() self.fixed_ip.ip_address = '198.51.100.5' self.fixed_ip.subnet.cidr = '198.51.100.0/24' @@ -109,40 +121,52 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') def test_update_amphora_listeners(self, mock_load_cert, mock_secret): mock_amphora = mock.MagicMock() - mock_amphora.id = uuidutils.generate_uuid() - mock_listener = mock.MagicMock() - mock_listener.id = uuidutils.generate_uuid() + mock_amphora.id = 'mock_amphora_id' + mock_amphora.api_version = API_VERSION + # mock_listener = mock.MagicMock() + # mock_listener.id = 'mock_listener_id' + # mock_listener.protocol = constants.PROTOCOL_HTTP + # mock_listener.connection_limit = constants.DEFAULT_CONNECTION_LIMIT + # mock_listener.tls_certificate_id = None + # mock_loadbalancer = mock.MagicMock() + # mock_loadbalancer.id = 'mock_lb_id' + # mock_loadbalancer.project_id = 'mock_lb_project' + # mock_loadbalancer.listeners = [mock_listener] + # mock_listener.load_balancer = mock_loadbalancer mock_secret.return_value = 'filename.pem' - mock_load_cert.return_value = {'tls_cert': None, 'sni_certs': [], - 'client_ca_cert': None} - self.driver.jinja.build_config.return_value = 'the_config' + mock_load_cert.return_value = { + 'tls_cert': self.sl.default_tls_container, 'sni_certs': [], + 'client_ca_cert': None} + self.driver.jinja_split.build_config.return_value = 'the_config' - self.driver.update_amphora_listeners(None, 1, [], + mock_empty_lb = mock.MagicMock() + mock_empty_lb.listeners = [] + self.driver.update_amphora_listeners(mock_empty_lb, mock_amphora, self.timeout_dict) mock_load_cert.assert_not_called() - self.driver.jinja.build_config.assert_not_called() - self.driver.client.upload_config.assert_not_called() - self.driver.client.reload_listener.assert_not_called() + self.driver.jinja_split.build_config.assert_not_called() + self.driver.clients[API_VERSION].upload_config.assert_not_called() + self.driver.clients[API_VERSION].reload_listener.assert_not_called() - self.driver.update_amphora_listeners([mock_listener], 0, - [mock_amphora], self.timeout_dict) - self.driver.client.upload_config.assert_called_once_with( - mock_amphora, mock_listener.id, 'the_config', + self.driver.update_amphora_listeners(self.lb, + mock_amphora, self.timeout_dict) + self.driver.clients[API_VERSION].upload_config.assert_called_once_with( + mock_amphora, self.sl.id, 'the_config', timeout_dict=self.timeout_dict) - self.driver.client.reload_listener(mock_amphora, mock_listener.id, - timeout_dict=self.timeout_dict) + self.driver.clients[API_VERSION].reload_listener( + mock_amphora, self.sl.id, timeout_dict=self.timeout_dict) mock_load_cert.reset_mock() - self.driver.jinja.build_config.reset_mock() - self.driver.client.upload_config.reset_mock() - self.driver.client.reload_listener.reset_mock() + self.driver.jinja_split.build_config.reset_mock() + self.driver.clients[API_VERSION].upload_config.reset_mock() + self.driver.clients[API_VERSION].reload_listener.reset_mock() mock_amphora.status = constants.DELETED - self.driver.update_amphora_listeners([mock_listener], 0, - [mock_amphora], self.timeout_dict) + self.driver.update_amphora_listeners(self.lb, + mock_amphora, self.timeout_dict) mock_load_cert.assert_not_called() - self.driver.jinja.build_config.assert_not_called() - self.driver.client.upload_config.assert_not_called() - self.driver.client.reload_listener.assert_not_called() + self.driver.jinja_split.build_config.assert_not_called() + self.driver.clients[API_VERSION].upload_config.assert_not_called() + self.driver.clients[API_VERSION].reload_listener.assert_not_called() @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' 'HaproxyAmphoraLoadBalancerDriver._process_secret') @@ -157,15 +181,13 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): mock_load_crt.side_effect = [{ 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts}, {'tls_cert': None, 'sni_certs': []}] - self.driver.client.get_cert_md5sum.side_effect = [ + self.driver.clients[API_VERSION].get_cert_md5sum.side_effect = [ exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'CA_CERT_MD5'] - self.driver.jinja.build_config.side_effect = ['fake_config'] - self.driver.client.get_listener_status.side_effect = [ - dict(status='ACTIVE')] + self.driver.jinja_split.build_config.side_effect = ['fake_config'] # Execute driver method - self.driver.update(self.sl, self.sv) + self.driver.update(self.lb) # verify result gcm_calls = [ @@ -178,8 +200,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sconts[1].id + '.pem', ignore=(404,)), ] - self.driver.client.get_cert_md5sum.assert_has_calls(gcm_calls, - any_order=True) + self.driver.clients[API_VERSION].get_cert_md5sum.assert_has_calls( + gcm_calls, any_order=True) # this is called three times (last MD5 matches) fp1 = b'\n'.join([sample_certs.X509_CERT, @@ -201,18 +223,21 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sconts[1].id + '.pem', fp3), ] - self.driver.client.upload_cert_pem.assert_has_calls(ucp_calls, - any_order=True) + self.driver.clients[API_VERSION].upload_cert_pem.assert_has_calls( + ucp_calls, any_order=True) # upload only one config file - self.driver.client.upload_config.assert_called_once_with( - self.amp, self.sl.id, 'fake_config') + self.driver.clients[API_VERSION].upload_config.assert_called_once_with( + self.amp, self.sl.id, 'fake_config', timeout_dict=None) # start should be called once - self.driver.client.reload_listener.assert_called_once_with( - self.amp, self.sl.id) + self.driver.clients[ + API_VERSION].reload_listener.assert_called_once_with( + self.amp, self.sl.id, timeout_dict=None) secret_calls = [ - mock.call(self.sl, self.sl.client_ca_tls_certificate_id), - mock.call(self.sl, self.sl.client_crl_container_id) + mock.call(self.sl, self.sl.client_ca_tls_certificate_id, self.amp, + self.sl.id), + mock.call(self.sl, self.sl.client_crl_container_id, self.amp, + self.sl.id) ] mock_secret.assert_has_calls(secret_calls) @@ -220,24 +245,27 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.udp_jinja.build_config.side_effect = ['fake_udp_config'] # Execute driver method - self.driver.update(self.sl_udp, self.sv) + self.driver.update(self.lb_udp) # upload only one config file - self.driver.client.upload_udp_config.assert_called_once_with( - self.amp, self.sl_udp.id, 'fake_udp_config') + self.driver.clients[ + API_VERSION].upload_udp_config.assert_called_once_with( + self.amp, self.sl_udp.id, 'fake_udp_config', timeout_dict=None) # start should be called once - self.driver.client.reload_listener.assert_called_once_with( - self.amp, self.sl_udp.id) + self.driver.clients[ + API_VERSION].reload_listener.assert_called_once_with( + self.amp, self.sl_udp.id, timeout_dict=None) def test_upload_cert_amp(self): self.driver.upload_cert_amp(self.amp, six.b('test')) - self.driver.client.update_cert_for_rotation.assert_called_once_with( + self.driver.clients[ + API_VERSION].update_cert_for_rotation.assert_called_once_with( self.amp, six.b('test')) @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') def test__process_tls_certificates_no_ca_cert(self, mock_load_crt): - sample_listener = sample_configs.sample_listener_tuple( + sample_listener = sample_configs_split.sample_listener_tuple( tls=True, sni=True) sconts = [] for sni_container in sample_listener.sni_containers: @@ -246,20 +274,21 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts } - self.driver.client.get_cert_md5sum.side_effect = [ + self.driver.clients[API_VERSION].get_cert_md5sum.side_effect = [ exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] - self.driver._process_tls_certificates(sample_listener) + self.driver._process_tls_certificates( + sample_listener, self.amp, sample_listener.load_balancer.id) gcm_calls = [ - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, self.sl.default_tls_container.id + '.pem', ignore=(404,)), - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, sconts[0].id + '.pem', ignore=(404,)), - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, sconts[1].id + '.pem', ignore=(404,)) ] - self.driver.client.get_cert_md5sum.assert_has_calls(gcm_calls, - any_order=True) + self.driver.clients[API_VERSION].get_cert_md5sum.assert_has_calls( + gcm_calls, any_order=True) fp1 = b'\n'.join([sample_certs.X509_CERT, sample_certs.X509_CERT_KEY, sample_certs.X509_IMDS]) + b'\n' @@ -270,23 +299,24 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sample_certs.X509_CERT_KEY_3, sample_certs.X509_IMDS]) + b'\n' ucp_calls = [ - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, self.sl.default_tls_container.id + '.pem', fp1), - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, sconts[0].id + '.pem', fp2), - mock.call(self.amp, self.sl.id, + mock.call(self.amp, self.lb.id, sconts[1].id + '.pem', fp3) ] - self.driver.client.upload_cert_pem.assert_has_calls(ucp_calls, - any_order=True) - self.assertEqual(3, self.driver.client.upload_cert_pem.call_count) + self.driver.clients[API_VERSION].upload_cert_pem.assert_has_calls( + ucp_calls, any_order=True) + self.assertEqual( + 3, self.driver.clients[API_VERSION].upload_cert_pem.call_count) @mock.patch('oslo_context.context.RequestContext') @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' - 'HaproxyAmphoraLoadBalancerDriver._apply') - def test_process_secret(self, mock_apply, mock_oslo): + 'HaproxyAmphoraLoadBalancerDriver._upload_cert') + def test_process_secret(self, mock_upload_cert, mock_oslo): # Test bypass if no secret_ref - sample_listener = sample_configs.sample_listener_tuple( + sample_listener = sample_configs_split.sample_listener_tuple( tls=True, sni=True) result = self.driver._process_secret(sample_listener, None) @@ -295,7 +325,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.cert_manager.get_secret.assert_not_called() # Test the secret process - sample_listener = sample_configs.sample_listener_tuple( + sample_listener = sample_configs_split.sample_listener_tuple( tls=True, sni=True, client_ca_cert=True) fake_context = 'fake context' fake_secret = b'fake cert' @@ -307,21 +337,23 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): ref_name = '{id}.pem'.format(id=ref_id) result = self.driver._process_secret( - sample_listener, sample_listener.client_ca_tls_certificate_id) + sample_listener, sample_listener.client_ca_tls_certificate_id, + self.amp, sample_listener.id) mock_oslo.assert_called_once_with( project_id=sample_listener.project_id) self.driver.cert_manager.get_secret.assert_called_once_with( fake_context, sample_listener.client_ca_tls_certificate_id) - mock_apply.assert_called_once_with( - self.driver._upload_cert, sample_listener, None, fake_secret, - ref_md5, ref_name) + mock_upload_cert.assert_called_once_with( + self.amp, sample_listener.id, pem=fake_secret, + md5=ref_md5, name=ref_name) self.assertEqual(ref_name, result) @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' 'HaproxyAmphoraLoadBalancerDriver._process_pool_certs') def test__process_listener_pool_certs(self, mock_pool_cert): - sample_listener = sample_configs.sample_listener_tuple(l7=True) + sample_listener = sample_configs_split.sample_listener_tuple( + l7=True) ref_pool_cert_1 = {'client_cert': '/some/fake/cert-1.pem'} ref_pool_cert_2 = {'client_cert': '/some/fake/cert-2.pem'} @@ -331,11 +363,14 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): ref_cert_dict = {'sample_pool_id_1': ref_pool_cert_1, 'sample_pool_id_2': ref_pool_cert_2} - result = self.driver._process_listener_pool_certs(sample_listener) + result = self.driver._process_listener_pool_certs( + sample_listener, self.amp, sample_listener.load_balancer.id) pool_certs_calls = [ - mock.call(sample_listener, sample_listener.default_pool), - mock.call(sample_listener, sample_listener.pools[1]) + mock.call(sample_listener, sample_listener.default_pool, + self.amp, sample_listener.load_balancer.id), + mock.call(sample_listener, sample_listener.pools[1], + self.amp, sample_listener.load_balancer.id) ] mock_pool_cert.assert_has_calls(pool_certs_calls, any_order=True) @@ -345,15 +380,15 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' 'HaproxyAmphoraLoadBalancerDriver._process_secret') @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' - 'HaproxyAmphoraLoadBalancerDriver._apply') + 'HaproxyAmphoraLoadBalancerDriver._upload_cert') @mock.patch('octavia.common.tls_utils.cert_parser.build_pem') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') def test__process_pool_certs(self, mock_load_certs, mock_build_pem, - mock_apply, mock_secret): + mock_upload_cert, mock_secret): fake_cert_dir = '/fake/cert/dir' conf = oslo_fixture.Config(cfg.CONF) conf.config(group="haproxy_amphora", base_cert_dir=fake_cert_dir) - sample_listener = sample_configs.sample_listener_tuple( + sample_listener = sample_configs_split.sample_listener_tuple( pool_cert=True, pool_ca_cert=True, pool_crl=True) cert_data_mock = mock.MagicMock() cert_data_mock.id = uuidutils.generate_uuid() @@ -376,89 +411,133 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): 'crl': ref_crl_path} mock_secret.side_effect = [ref_ca_name, ref_crl_name] - result = self.driver._process_pool_certs(sample_listener, - sample_listener.default_pool) + result = self.driver._process_pool_certs( + sample_listener, sample_listener.default_pool, self.amp, + sample_listener.load_balancer.id) secret_calls = [ mock.call(sample_listener, - sample_listener.default_pool.ca_tls_certificate_id), + sample_listener.default_pool.ca_tls_certificate_id, + self.amp, sample_listener.load_balancer.id), mock.call(sample_listener, - sample_listener.default_pool.crl_container_id)] + sample_listener.default_pool.crl_container_id, + self.amp, sample_listener.load_balancer.id)] mock_build_pem.assert_called_once_with(cert_data_mock) - mock_apply.assert_called_once_with( - self.driver._upload_cert, sample_listener, None, fake_pem, - ref_md5, ref_name) + mock_upload_cert.assert_called_once_with( + self.amp, sample_listener.load_balancer.id, pem=fake_pem, + md5=ref_md5, name=ref_name) mock_secret.assert_has_calls(secret_calls) self.assertEqual(ref_result, result) - def test_stop(self): - self.driver.client.stop_listener.__name__ = 'stop_listener' - # Execute driver method - self.driver.stop(self.sl, self.sv) - self.driver.client.stop_listener.assert_called_once_with( - self.amp, self.sl.id) - - def test_udp_stop(self): - self.driver.client.stop_listener.__name__ = 'stop_listener' - # Execute driver method - UDP case - self.driver.stop(self.sl_udp, self.sv) - self.driver.client.stop_listener.assert_called_once_with( - self.amp, self.sl_udp.id) - def test_start(self): amp1 = mock.MagicMock() + amp1.api_version = API_VERSION amp2 = mock.MagicMock() + amp2.api_version = API_VERSION amp2.status = constants.DELETED + loadbalancer = mock.MagicMock() + loadbalancer.id = uuidutils.generate_uuid() + loadbalancer.amphorae = [amp1, amp2] + loadbalancer.vip = self.sv listener = mock.MagicMock() listener.id = uuidutils.generate_uuid() - listener.load_balancer.amphorae = [amp1, amp2] - listener.protocol = 'listener_protocol' - del listener.listeners - self.driver.client.start_listener.__name__ = 'start_listener' + listener.protocol = constants.PROTOCOL_HTTP + loadbalancer.listeners = [listener] + listener.load_balancer = loadbalancer + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' # Execute driver method - self.driver.start(listener, self.sv) - self.driver.client.start_listener.assert_called_once_with( + self.driver.start(loadbalancer) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( amp1, listener.id) def test_start_with_amphora(self): # Execute driver method amp = mock.MagicMock() - self.driver.client.start_listener.__name__ = 'start_listener' - self.driver.start(self.sl, self.sv, self.amp) - self.driver.client.start_listener.assert_called_once_with( + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' + self.driver.start(self.lb, self.amp) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( self.amp, self.sl.id) - self.driver.client.start_listener.reset_mock() + self.driver.clients[API_VERSION].start_listener.reset_mock() amp.status = constants.DELETED - self.driver.start(self.sl, self.sv, amp) - self.driver.client.start_listener.assert_not_called() + self.driver.start(self.lb, amp) + self.driver.clients[API_VERSION].start_listener.assert_not_called() def test_udp_start(self): - self.driver.client.start_listener.__name__ = 'start_listener' + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' # Execute driver method - self.driver.start(self.sl_udp, self.sv) - self.driver.client.start_listener.assert_called_once_with( + self.driver.start(self.lb_udp) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( self.amp, self.sl_udp.id) - def test_delete(self): - self.driver.client.delete_listener.__name__ = 'delete_listener' + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_secret') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + @mock.patch('octavia.common.tls_utils.cert_parser.get_host_names') + def test_delete_second_listener(self, mock_cert, mock_load_crt, + mock_secret): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + sl = sample_configs_split.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + sl2 = sample_configs_split.sample_listener_tuple( + id='sample_listener_id_2') + sl.load_balancer.listeners.append(sl2) + mock_cert.return_value = {'cn': sample_certs.X509_CERT_CN} + mock_secret.side_effect = ['filename.pem', 'crl-filename.pem'] + sconts = [] + for sni_container in self.sl.sni_containers: + sconts.append(sni_container.tls_container) + mock_load_crt.side_effect = [{ + 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts}, + {'tls_cert': None, 'sni_certs': []}] + self.driver.jinja_split.build_config.side_effect = ['fake_config'] + # Execute driver method - self.driver.delete(self.sl, self.sv) - self.driver.client.delete_listener.assert_called_once_with( + self.driver.delete(sl) + + # Now just make sure we did a delete + self.driver.clients[ + API_VERSION].delete_listener.assert_called_once_with( self.amp, self.sl.id) - def test_udp_delete(self): - self.driver.client.delete_listener.__name__ = 'delete_listener' + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_delete_last_listener(self, mock_load_crt): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + sl = sample_configs_split.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + mock_load_crt.side_effect = [{ + 'tls_cert': sl.default_tls_container, 'sni_certs': None}] # Execute driver method - self.driver.delete(self.sl_udp, self.sv) - self.driver.client.delete_listener.assert_called_once_with( + self.driver.delete(sl) + self.driver.clients[ + API_VERSION].delete_listener.assert_called_once_with( + self.amp, sl.id) + + def test_udp_delete(self): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + # Execute driver method + self.driver.delete(self.sl_udp) + self.driver.clients[ + API_VERSION].delete_listener.assert_called_once_with( self.amp, self.sl_udp.id) def test_get_info(self): - self.driver.client.get_info.return_value = 'FAKE_INFO' + expected_info = {'haproxy_version': '1.6.3-1ubuntu0.1', + 'api_version': API_VERSION} result = self.driver.get_info(self.amp) - self.assertEqual('FAKE_INFO', result) + self.assertEqual(expected_info, result) def test_get_diagnostics(self): # TODO(johnsom) Implement once this exists on the amphora agent. @@ -477,7 +556,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): amphorae_network_config.get().vip_subnet.host_routes = self.host_routes amphorae_network_config.get().vrrp_port = self.port self.driver.post_vip_plug(self.amp, self.lb, amphorae_network_config) - self.driver.client.plug_vip.assert_called_once_with( + self.driver.clients[API_VERSION].plug_vip.assert_called_once_with( self.amp, self.lb.vip.ip_address, self.subnet_info) def test_post_network_plug(self): @@ -486,16 +565,16 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): fixed_ips=[], network=self.network) self.driver.post_network_plug(self.amp, port) - self.driver.client.plug_network.assert_called_once_with( + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[], mtu=FAKE_MTU)) - self.driver.client.plug_network.reset_mock() + self.driver.clients[API_VERSION].plug_network.reset_mock() # Test fixed IP path self.driver.post_network_plug(self.amp, self.port) - self.driver.client.plug_network.assert_called_once_with( + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[dict(ip_address='198.51.100.5', subnet_cidr='198.51.100.0/24', @@ -534,27 +613,47 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, {'destination': DEST2, 'nexthop': NEXTHOP}]} ] - self.driver.client.plug_network.assert_called_once_with( + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=expected_fixed_ips, mtu=FAKE_MTU)) def test_get_vrrp_interface(self): self.driver.get_vrrp_interface(self.amp) - self.driver.client.get_interface.assert_called_once_with( + self.driver.clients[API_VERSION].get_interface.assert_called_once_with( self.amp, self.amp.vrrp_ip, timeout_dict=None) def test_get_haproxy_versions(self): - ref_versions = ['1', '6'] - self.driver.client.get_info.return_value = { - 'haproxy_version': u'1.6.3-1ubuntu0.1'} + ref_haproxy_versions = ['1', '6'] result = self.driver._get_haproxy_versions(self.amp) - self.driver.client.get_info.assert_called_once_with(self.amp) - self.assertEqual(ref_versions, result) + self.driver.clients[API_VERSION].get_info.assert_called_once_with( + self.amp) + self.assertEqual(ref_haproxy_versions, result) + + def test_populate_amphora_api_version(self): + + # Normal path, populate the version + # clear out any previous values + ref_haproxy_version = list(map(int, API_VERSION.split('.'))) + mock_amp = mock.MagicMock() + mock_amp.api_version = None + result = self.driver._populate_amphora_api_version(mock_amp) + self.assertEqual(API_VERSION, mock_amp.api_version) + self.assertEqual(ref_haproxy_version, result) + + # Existing version passed in + fake_version = '9999.9999' + ref_haproxy_version = list(map(int, fake_version.split('.'))) + mock_amp = mock.MagicMock() + mock_amp.api_version = fake_version + result = self.driver._populate_amphora_api_version(mock_amp) + self.assertEqual(fake_version, mock_amp.api_version) + self.assertEqual(ref_haproxy_version, result) def test_update_amphora_agent_config(self): self.driver.update_amphora_agent_config(self.amp, six.b('test')) - self.driver.client.update_agent_config.assert_called_once_with( + self.driver.clients[ + API_VERSION].update_agent_config.assert_called_once_with( self.amp, six.b('test'), timeout_dict=None) @@ -562,9 +661,11 @@ class TestAmphoraAPIClientTest(base.TestCase): def setUp(self): super(TestAmphoraAPIClientTest, self).setUp() - self.driver = driver.AmphoraAPIClient() - self.base_url = "https://127.0.0.1:9443/0.5" - self.amp = models.Amphora(lb_network_ip='127.0.0.1', compute_id='123') + self.driver = driver.AmphoraAPIClient0_5() + self.base_url = "https://192.0.2.77:9443/" + self.base_url_ver = self.base_url + API_VERSION + self.amp = models.Amphora(lb_network_ip='192.0.2.77', compute_id='123') + self.amp.api_version = API_VERSION self.port_info = dict(mac_address=FAKE_MAC_ADDRESS) # Override with much lower values for testing purposes.. conf = oslo_fixture.Config(cfg.CONF) @@ -583,10 +684,10 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_base_url(self): url = self.driver._base_url(FAKE_IP) - self.assertEqual('https://192.0.2.10:9443/0.5/', url) - url = self.driver._base_url(FAKE_IPV6) + self.assertEqual('https://192.0.2.10:9443/', url) + url = self.driver._base_url(FAKE_IPV6, self.amp.api_version) self.assertEqual('https://[2001:db8::cafe]:9443/0.5/', url) - url = self.driver._base_url(FAKE_IPV6_LLA) + url = self.driver._base_url(FAKE_IPV6_LLA, self.amp.api_version) self.assertEqual('https://[fe80::00ff:fe00:cafe%o-hm0]:9443/0.5/', url) @mock.patch('requests.Session.get', side_effect=requests.ConnectionError) @@ -596,38 +697,52 @@ class TestAmphoraAPIClientTest(base.TestCase): self.driver.request, 'get', self.amp, 'unavailableURL', self.timeout_dict) + @requests_mock.mock() + def test_get_api_version(self, mock_requests): + ref_api_version = {'api_version': '0.1'} + mock_requests.get('{base}/'.format(base=self.base_url), + json=ref_api_version) + result = self.driver.get_api_version(self.amp) + self.assertEqual(ref_api_version, result) + + @requests_mock.mock() + def test_get_api_version_not_found(self, mock_requests): + mock_requests.get('{base}/'.format(base=self.base_url), + status_code=404) + self.assertRaises(exc.NotFound, self.driver.get_api_version, self.amp) + @requests_mock.mock() def test_get_info(self, m): info = {"hostname": "some_hostname", "version": "some_version", "api_version": "0.5", "uuid": FAKE_UUID_1} - m.get("{base}/info".format(base=self.base_url), + m.get("{base}/info".format(base=self.base_url_ver), json=info) information = self.driver.get_info(self.amp) self.assertEqual(info, information) @requests_mock.mock() def test_get_info_unauthorized(self, m): - m.get("{base}/info".format(base=self.base_url), + m.get("{base}/info".format(base=self.base_url_ver), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.get_info, self.amp) @requests_mock.mock() def test_get_info_missing(self, m): - m.get("{base}/info".format(base=self.base_url), + m.get("{base}/info".format(base=self.base_url_ver), status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.get_info, self.amp) @requests_mock.mock() def test_get_info_server_error(self, m): - m.get("{base}/info".format(base=self.base_url), + m.get("{base}/info".format(base=self.base_url_ver), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.get_info, self.amp) @requests_mock.mock() def test_get_info_service_unavailable(self, m): - m.get("{base}/info".format(base=self.base_url), + m.get("{base}/info".format(base=self.base_url_ver), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.get_info, self.amp) @@ -638,34 +753,34 @@ class TestAmphoraAPIClientTest(base.TestCase): "api_version": "0.5", "uuid": FAKE_UUID_1, "network_tx": "some_tx", "network_rx": "some_rx", "active": True, "haproxy_count": 10} - m.get("{base}/details".format(base=self.base_url), + m.get("{base}/details".format(base=self.base_url_ver), json=details) amp_details = self.driver.get_details(self.amp) self.assertEqual(details, amp_details) @requests_mock.mock() def test_get_details_unauthorized(self, m): - m.get("{base}/details".format(base=self.base_url), + m.get("{base}/details".format(base=self.base_url_ver), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.get_details, self.amp) @requests_mock.mock() def test_get_details_missing(self, m): - m.get("{base}/details".format(base=self.base_url), + m.get("{base}/details".format(base=self.base_url_ver), status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.get_details, self.amp) @requests_mock.mock() def test_get_details_server_error(self, m): - m.get("{base}/details".format(base=self.base_url), + m.get("{base}/details".format(base=self.base_url_ver), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.get_details, self.amp) @requests_mock.mock() def test_get_details_service_unavailable(self, m): - m.get("{base}/details".format(base=self.base_url), + m.get("{base}/details".format(base=self.base_url_ver), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.get_details, self.amp) @@ -674,21 +789,21 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_get_all_listeners(self, m): listeners = [{"status": "ONLINE", "provisioning_status": "ACTIVE", "type": "PASSIVE", "uuid": FAKE_UUID_1}] - m.get("{base}/listeners".format(base=self.base_url), + m.get("{base}/listeners".format(base=self.base_url_ver), json=listeners) all_listeners = self.driver.get_all_listeners(self.amp) self.assertEqual(listeners, all_listeners) @requests_mock.mock() def test_get_all_listeners_unauthorized(self, m): - m.get("{base}/listeners".format(base=self.base_url), + m.get("{base}/listeners".format(base=self.base_url_ver), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.get_all_listeners, self.amp) @requests_mock.mock() def test_get_all_listeners_missing(self, m): - m.get("{base}/listeners".format(base=self.base_url), + m.get("{base}/listeners".format(base=self.base_url_ver), status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.get_all_listeners, @@ -696,96 +811,29 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_get_all_listeners_server_error(self, m): - m.get("{base}/listeners".format(base=self.base_url), + m.get("{base}/listeners".format(base=self.base_url_ver), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.get_all_listeners, self.amp) @requests_mock.mock() def test_get_all_listeners_service_unavailable(self, m): - m.get("{base}/listeners".format(base=self.base_url), + m.get("{base}/listeners".format(base=self.base_url_ver), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.get_all_listeners, self.amp) - @requests_mock.mock() - def test_get_listener_status(self, m): - listener = {"status": "ONLINE", "provisioning_status": "ACTIVE", - "type": "PASSIVE", "uuid": FAKE_UUID_1} - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - json=listener) - status = self.driver.get_listener_status(self.amp, FAKE_UUID_1) - self.assertEqual(listener, status) - - @requests_mock.mock() - def test_get_udp_listener_status(self, m): - udp_listener = {"status": "ACTIVE", "type": "lvs", - "uuid": FAKE_UUID_1, - "pools": [{ - "UDP-Listener-%s-pool" % FAKE_UUID_1: - { - "status": "UP", - "members": [ - {FAKE_MEMBER_IP_PORT_NAME_1: "DOWN"}, - {FAKE_MEMBER_IP_PORT_NAME_2: "ACTIVE"}, - ] - } - }]} - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - json=udp_listener) - status = self.driver.get_listener_status(self.amp, FAKE_UUID_1) - self.assertEqual(udp_listener, status) - - @requests_mock.mock() - def test_get_listener_status_unauthorized(self, m): - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=401) - self.assertRaises(exc.Unauthorized, - self.driver.get_listener_status, self.amp, - FAKE_UUID_1) - - @requests_mock.mock() - def test_get_listener_status_missing(self, m): - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=404, - headers={'content-type': 'application/json'}) - self.assertRaises(exc.NotFound, - self.driver.get_listener_status, self.amp, - FAKE_UUID_1) - - @requests_mock.mock() - def test_get_listener_status_server_error(self, m): - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=500) - self.assertRaises(exc.InternalServerError, - self.driver.get_listener_status, self.amp, - FAKE_UUID_1) - - @requests_mock.mock() - def test_get_listener_status_service_unavailable(self, m): - m.get("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=503) - self.assertRaises(exc.ServiceUnavailable, - self.driver.get_listener_status, self.amp, - FAKE_UUID_1) - @requests_mock.mock() def test_start_listener(self, m): m.put("{base}/listeners/{listener_id}/start".format( - base=self.base_url, listener_id=FAKE_UUID_1)) + base=self.base_url_ver, listener_id=FAKE_UUID_1)) self.driver.start_listener(self.amp, FAKE_UUID_1) self.assertTrue(m.called) @requests_mock.mock() def test_start_listener_missing(self, m): m.put("{base}/listeners/{listener_id}/start".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.start_listener, @@ -794,7 +842,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_start_listener_unauthorized(self, m): m.put("{base}/listeners/{listener_id}/start".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.start_listener, self.amp, FAKE_UUID_1) @@ -802,7 +850,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_start_listener_server_error(self, m): m.put("{base}/listeners/{listener_id}/start".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.start_listener, self.amp, FAKE_UUID_1) @@ -810,62 +858,22 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_start_listener_service_unavailable(self, m): m.put("{base}/listeners/{listener_id}/start".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.start_listener, self.amp, FAKE_UUID_1) - @requests_mock.mock() - def test_stop_listener(self, m): - m.put("{base}/listeners/{listener_id}/stop".format( - base=self.base_url, listener_id=FAKE_UUID_1)) - self.driver.stop_listener(self.amp, FAKE_UUID_1) - self.assertTrue(m.called) - - @requests_mock.mock() - def test_stop_listener_missing(self, m): - m.put("{base}/listeners/{listener_id}/stop".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=404, - headers={'content-type': 'application/json'}) - self.assertRaises(exc.NotFound, self.driver.stop_listener, - self.amp, FAKE_UUID_1) - - @requests_mock.mock() - def test_stop_listener_unauthorized(self, m): - m.put("{base}/listeners/{listener_id}/stop".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=401) - self.assertRaises(exc.Unauthorized, self.driver.stop_listener, - self.amp, FAKE_UUID_1) - - @requests_mock.mock() - def test_stop_listener_server_error(self, m): - m.put("{base}/listeners/{listener_id}/stop".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=500) - self.assertRaises(exc.InternalServerError, self.driver.stop_listener, - self.amp, FAKE_UUID_1) - - @requests_mock.mock() - def test_stop_listener_service_unavailable(self, m): - m.put("{base}/listeners/{listener_id}/stop".format( - base=self.base_url, listener_id=FAKE_UUID_1), - status_code=503) - self.assertRaises(exc.ServiceUnavailable, self.driver.stop_listener, - self.amp, FAKE_UUID_1) - @requests_mock.mock() def test_delete_listener(self, m): m.delete("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), json={}) + base=self.base_url_ver, listener_id=FAKE_UUID_1), json={}) self.driver.delete_listener(self.amp, FAKE_UUID_1) self.assertTrue(m.called) @requests_mock.mock() def test_delete_listener_missing(self, m): m.delete("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=404, headers={'content-type': 'application/json'}) self.driver.delete_listener(self.amp, FAKE_UUID_1) @@ -874,7 +882,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_delete_listener_unauthorized(self, m): m.delete("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.delete_listener, self.amp, FAKE_UUID_1) @@ -882,7 +890,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_delete_listener_server_error(self, m): m.delete("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.delete_listener, self.amp, FAKE_UUID_1) @@ -890,7 +898,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_delete_listener_service_unavailable(self, m): m.delete("{base}/listeners/{listener_id}".format( - base=self.base_url, listener_id=FAKE_UUID_1), + base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.delete_listener, self.amp, FAKE_UUID_1) @@ -898,7 +906,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_upload_cert_pem(self, m): m.put("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME)) self.driver.upload_cert_pem(self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, @@ -908,7 +916,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_upload_invalid_cert_pem(self, m): m.put("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=400) self.assertRaises(exc.InvalidRequest, self.driver.upload_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, @@ -917,7 +925,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_upload_cert_pem_unauthorized(self, m): m.put("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.upload_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, @@ -926,7 +934,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_upload_cert_pem_server_error(self, m): m.put("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.upload_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, @@ -935,7 +943,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_upload_cert_pem_service_unavailable(self, m): m.put("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.upload_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, @@ -943,35 +951,39 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_update_cert_for_rotation(self, m): - m.put("{base}/certificate".format(base=self.base_url)) + m.put("{base}/certificate".format(base=self.base_url_ver)) resp_body = self.driver.update_cert_for_rotation(self.amp, "some_file") self.assertEqual(200, resp_body.status_code) @requests_mock.mock() def test_update_invalid_cert_for_rotation(self, m): - m.put("{base}/certificate".format(base=self.base_url), status_code=400) + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=400) self.assertRaises(exc.InvalidRequest, self.driver.update_cert_for_rotation, self.amp, "some_file") @requests_mock.mock() def test_update_cert_for_rotation_unauthorized(self, m): - m.put("{base}/certificate".format(base=self.base_url), status_code=401) + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=401) self.assertRaises(exc.Unauthorized, self.driver.update_cert_for_rotation, self.amp, "some_file") @requests_mock.mock() def test_update_cert_for_rotation_error(self, m): - m.put("{base}/certificate".format(base=self.base_url), status_code=500) + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=500) self.assertRaises(exc.InternalServerError, self.driver.update_cert_for_rotation, self.amp, "some_file") @requests_mock.mock() def test_update_cert_for_rotation_unavailable(self, m): - m.put("{base}/certificate".format(base=self.base_url), status_code=503) + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.update_cert_for_rotation, self.amp, "some_file") @@ -980,7 +992,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_get_cert_5sum(self, m): md5sum = {"md5sum": "some_real_sum"} m.get("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), json=md5sum) sum_test = self.driver.get_cert_md5sum(self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -989,7 +1001,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_get_cert_5sum_missing(self, m): m.get("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.get_cert_md5sum, @@ -998,7 +1010,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_get_cert_5sum_unauthorized(self, m): m.get("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.get_cert_md5sum, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1006,7 +1018,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_get_cert_5sum_server_error(self, m): m.get("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.get_cert_md5sum, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1014,7 +1026,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_get_cert_5sum_service_unavailable(self, m): m.get("{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.get_cert_md5sum, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1023,7 +1035,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_delete_cert_pem(self, m): m.delete( "{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME)) self.driver.delete_cert_pem(self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1033,7 +1045,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_delete_cert_pem_missing(self, m): m.delete( "{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=404, headers={'content-type': 'application/json'}) self.driver.delete_cert_pem(self.amp, FAKE_UUID_1, @@ -1044,7 +1056,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_delete_cert_pem_unauthorized(self, m): m.delete( "{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.delete_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1053,7 +1065,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_delete_cert_pem_server_error(self, m): m.delete( "{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.delete_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1062,7 +1074,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_delete_cert_pem_service_unavailable(self, m): m.delete( "{base}/listeners/{listener_id}/certificates/{filename}".format( - base=self.base_url, listener_id=FAKE_UUID_1, + base=self.base_url_ver, listener_id=FAKE_UUID_1, filename=FAKE_PEM_FILENAME), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.delete_cert_pem, self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) @@ -1073,7 +1085,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/{" "amphora_id}/{listener_id}/haproxy".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), json=config) self.driver.upload_config(self.amp, FAKE_UUID_1, @@ -1086,7 +1098,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/{" "amphora_id}/{listener_id}/haproxy".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=400) self.assertRaises(exc.InvalidRequest, self.driver.upload_config, @@ -1098,7 +1110,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/{" "amphora_id}/{listener_id}/haproxy".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.upload_config, @@ -1110,7 +1122,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/{" "amphora_id}/{listener_id}/haproxy".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.upload_config, @@ -1122,7 +1134,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/{" "amphora_id}/{listener_id}/haproxy".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.upload_config, @@ -1134,7 +1146,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/" "{amphora_id}/{listener_id}/udp_listener".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), json=config) self.driver.upload_udp_config(self.amp, FAKE_UUID_1, config) @@ -1146,7 +1158,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/" "{amphora_id}/{listener_id}/udp_listener".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=400) self.assertRaises(exc.InvalidRequest, self.driver.upload_udp_config, @@ -1158,7 +1170,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/" "{amphora_id}/{listener_id}/udp_listener".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.upload_udp_config, @@ -1170,7 +1182,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/" "{amphora_id}/{listener_id}/udp_listener".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, @@ -1183,7 +1195,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.put( "{base}/listeners/" "{amphora_id}/{listener_id}/udp_listener".format( - amphora_id=self.amp.id, base=self.base_url, + amphora_id=self.amp.id, base=self.base_url_ver, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, @@ -1193,7 +1205,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_plug_vip(self, m): m.post("{base}/plug/vip/{vip}".format( - base=self.base_url, vip=FAKE_IP) + base=self.base_url_ver, vip=FAKE_IP) ) self.driver.plug_vip(self.amp, FAKE_IP, self.subnet_info) self.assertTrue(m.called) @@ -1201,7 +1213,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_plug_vip_api_not_ready(self, m): m.post("{base}/plug/vip/{vip}".format( - base=self.base_url, vip=FAKE_IP), + base=self.base_url_ver, vip=FAKE_IP), status_code=404, headers={'content-type': 'text/html'} ) self.assertRaises(driver_except.TimeOutException, @@ -1212,7 +1224,7 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_plug_network(self, m): m.post("{base}/plug/network".format( - base=self.base_url) + base=self.base_url_ver) ) self.driver.plug_network(self.amp, self.port_info) self.assertTrue(m.called) @@ -1221,7 +1233,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_upload_vrrp_config(self, m): config = '{"name": "bad_config"}' m.put("{base}/vrrp/upload".format( - base=self.base_url) + base=self.base_url_ver) ) self.driver.upload_vrrp_config(self.amp, config) self.assertTrue(m.called) @@ -1229,7 +1241,8 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_vrrp_action(self, m): action = 'start' - m.put("{base}/vrrp/{action}".format(base=self.base_url, action=action)) + m.put("{base}/vrrp/{action}".format(base=self.base_url_ver, + action=action)) self.driver._vrrp_action(action, self.amp) self.assertTrue(m.called) @@ -1237,14 +1250,14 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_get_interface(self, m): interface = [{"interface": "eth1"}] ip_addr = '192.51.100.1' - m.get("{base}/interface/{ip_addr}".format(base=self.base_url, + m.get("{base}/interface/{ip_addr}".format(base=self.base_url_ver, ip_addr=ip_addr), json=interface) self.driver.get_interface(self.amp, ip_addr) self.assertTrue(m.called) m.register_uri('GET', - self.base_url + '/interface/' + ip_addr, + self.base_url_ver + '/interface/' + ip_addr, status_code=500, reason='FAIL', json='FAIL') self.assertRaises(exc.InternalServerError, self.driver.get_interface, @@ -1252,13 +1265,13 @@ class TestAmphoraAPIClientTest(base.TestCase): @requests_mock.mock() def test_update_agent_config(self, m): - m.put("{base}/config".format(base=self.base_url)) + m.put("{base}/config".format(base=self.base_url_ver)) resp_body = self.driver.update_agent_config(self.amp, "some_file") self.assertEqual(200, resp_body.status_code) @requests_mock.mock() def test_update_agent_config_error(self, m): - m.put("{base}/config".format(base=self.base_url), status_code=500) + m.put("{base}/config".format(base=self.base_url_ver), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.update_agent_config, self.amp, "some_file") diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py new file mode 100644 index 0000000000..4b4988dda8 --- /dev/null +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py @@ -0,0 +1,1299 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2015 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import hashlib + +import mock +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture +from oslo_utils import uuidutils +import requests +import requests_mock +import six + +from octavia.amphorae.driver_exceptions import exceptions as driver_except +from octavia.amphorae.drivers.haproxy import exceptions as exc +from octavia.amphorae.drivers.haproxy import rest_api_driver as driver +from octavia.common import constants +from octavia.db import models +from octavia.network import data_models as network_models +from octavia.tests.unit import base +from octavia.tests.unit.common.sample_configs import sample_certs +from octavia.tests.unit.common.sample_configs import sample_configs_combined + +API_VERSION = '1.0' +FAKE_CIDR = '198.51.100.0/24' +FAKE_GATEWAY = '192.51.100.1' +FAKE_IP = '192.0.2.10' +FAKE_IPV6 = '2001:db8::cafe' +FAKE_IPV6_LLA = 'fe80::00ff:fe00:cafe' +FAKE_PEM_FILENAME = "file_name" +FAKE_UUID_1 = uuidutils.generate_uuid() +FAKE_VRRP_IP = '10.1.0.1' +FAKE_MAC_ADDRESS = '123' +FAKE_MTU = 1450 +FAKE_MEMBER_IP_PORT_NAME_1 = "10.0.0.10:1003" +FAKE_MEMBER_IP_PORT_NAME_2 = "10.0.0.11:1004" + + +class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): + + def setUp(self): + super(TestHaproxyAmphoraLoadBalancerDriverTest, self).setUp() + + DEST1 = '198.51.100.0/24' + DEST2 = '203.0.113.0/24' + NEXTHOP = '192.0.2.1' + + self.driver = driver.HaproxyAmphoraLoadBalancerDriver() + + self.driver.cert_manager = mock.MagicMock() + self.driver.cert_parser = mock.MagicMock() + self.driver.clients = { + 'base': mock.MagicMock(), + API_VERSION: mock.MagicMock()} + self.driver.clients['base'].get_api_version.return_value = { + 'api_version': API_VERSION} + self.driver.clients[ + API_VERSION].get_info.return_value = { + 'haproxy_version': u'1.6.3-1ubuntu0.1', + 'api_version': API_VERSION} + self.driver.jinja_combo = mock.MagicMock() + self.driver.udp_jinja = mock.MagicMock() + + # Build sample Listener and VIP configs + self.sl = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + self.sl_udp = sample_configs_combined.sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, + persistence_timeout=33, + persistence_granularity='255.255.0.0', + monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT) + self.pool_has_cert = sample_configs_combined.sample_pool_tuple( + pool_cert=True, pool_ca_cert=True, pool_crl=True) + self.amp = self.sl.load_balancer.amphorae[0] + self.sv = sample_configs_combined.sample_vip_tuple() + self.lb = self.sl.load_balancer + self.lb_udp = ( + sample_configs_combined.sample_lb_with_udp_listener_tuple()) + self.fixed_ip = mock.MagicMock() + self.fixed_ip.ip_address = '198.51.100.5' + self.fixed_ip.subnet.cidr = '198.51.100.0/24' + self.network = network_models.Network(mtu=FAKE_MTU) + self.port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[self.fixed_ip], + network=self.network) + + self.host_routes = [network_models.HostRoute(destination=DEST1, + nexthop=NEXTHOP), + network_models.HostRoute(destination=DEST2, + nexthop=NEXTHOP)] + host_routes_data = [{'destination': DEST1, 'nexthop': NEXTHOP}, + {'destination': DEST2, 'nexthop': NEXTHOP}] + self.subnet_info = {'subnet_cidr': FAKE_CIDR, + 'gateway': FAKE_GATEWAY, + 'mac_address': FAKE_MAC_ADDRESS, + 'vrrp_ip': self.amp.vrrp_ip, + 'mtu': FAKE_MTU, + 'host_routes': host_routes_data} + + self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1, + constants.REQ_READ_TIMEOUT: 2, + constants.CONN_MAX_RETRIES: 3, + constants.CONN_RETRY_INTERVAL: 4} + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_secret') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_update_amphora_listeners(self, mock_load_cert, mock_secret): + mock_amphora = mock.MagicMock() + mock_amphora.id = 'mock_amphora_id' + mock_amphora.api_version = API_VERSION + # mock_listener = mock.MagicMock() + # mock_listener.id = 'mock_listener_id' + # mock_listener.protocol = constants.PROTOCOL_HTTP + # mock_listener.connection_limit = constants.DEFAULT_CONNECTION_LIMIT + # mock_listener.tls_certificate_id = None + # mock_loadbalancer = mock.MagicMock() + # mock_loadbalancer.id = 'mock_lb_id' + # mock_loadbalancer.project_id = 'mock_lb_project' + # mock_loadbalancer.listeners = [mock_listener] + # mock_listener.load_balancer = mock_loadbalancer + mock_secret.return_value = 'filename.pem' + mock_load_cert.return_value = { + 'tls_cert': self.sl.default_tls_container, 'sni_certs': [], + 'client_ca_cert': None} + self.driver.jinja_combo.build_config.return_value = 'the_config' + + mock_empty_lb = mock.MagicMock() + mock_empty_lb.listeners = [] + self.driver.update_amphora_listeners(mock_empty_lb, mock_amphora, + self.timeout_dict) + mock_load_cert.assert_not_called() + self.driver.jinja_combo.build_config.assert_not_called() + self.driver.clients[API_VERSION].upload_config.assert_not_called() + self.driver.clients[API_VERSION].reload_listener.assert_not_called() + + self.driver.update_amphora_listeners(self.lb, + mock_amphora, self.timeout_dict) + self.driver.clients[API_VERSION].upload_config.assert_called_once_with( + mock_amphora, self.lb.id, 'the_config', + timeout_dict=self.timeout_dict) + self.driver.clients[API_VERSION].reload_listener( + mock_amphora, self.lb.id, timeout_dict=self.timeout_dict) + + mock_load_cert.reset_mock() + self.driver.jinja_combo.build_config.reset_mock() + self.driver.clients[API_VERSION].upload_config.reset_mock() + self.driver.clients[API_VERSION].reload_listener.reset_mock() + mock_amphora.status = constants.DELETED + self.driver.update_amphora_listeners(self.lb, + mock_amphora, self.timeout_dict) + mock_load_cert.assert_not_called() + self.driver.jinja_combo.build_config.assert_not_called() + self.driver.clients[API_VERSION].upload_config.assert_not_called() + self.driver.clients[API_VERSION].reload_listener.assert_not_called() + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_secret') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + @mock.patch('octavia.common.tls_utils.cert_parser.get_host_names') + def test_update(self, mock_cert, mock_load_crt, mock_secret): + mock_cert.return_value = {'cn': sample_certs.X509_CERT_CN} + mock_secret.side_effect = ['filename.pem', 'crl-filename.pem'] + sconts = [] + for sni_container in self.sl.sni_containers: + sconts.append(sni_container.tls_container) + mock_load_crt.side_effect = [{ + 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts}, + {'tls_cert': None, 'sni_certs': []}] + self.driver.clients[API_VERSION].get_cert_md5sum.side_effect = [ + exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'CA_CERT_MD5'] + self.driver.jinja_combo.build_config.side_effect = ['fake_config'] + + # Execute driver method + self.driver.update(self.lb) + + # verify result + gcm_calls = [ + mock.call(self.amp, self.lb.id, + self.sl.default_tls_container.id + '.pem', + ignore=(404,)), + mock.call(self.amp, self.lb.id, + sconts[0].id + '.pem', ignore=(404,)), + mock.call(self.amp, self.lb.id, + sconts[1].id + '.pem', ignore=(404,)), + ] + + self.driver.clients[API_VERSION].get_cert_md5sum.assert_has_calls( + gcm_calls, any_order=True) + + # this is called three times (last MD5 matches) + fp1 = b'\n'.join([sample_certs.X509_CERT, + sample_certs.X509_CERT_KEY, + sample_certs.X509_IMDS]) + b'\n' + fp2 = b'\n'.join([sample_certs.X509_CERT_2, + sample_certs.X509_CERT_KEY_2, + sample_certs.X509_IMDS]) + b'\n' + fp3 = b'\n'.join([sample_certs.X509_CERT_3, + sample_certs.X509_CERT_KEY_3, + sample_certs.X509_IMDS]) + b'\n' + + ucp_calls = [ + mock.call(self.amp, self.lb.id, + self.sl.default_tls_container.id + '.pem', fp1), + mock.call(self.amp, self.lb.id, + sconts[0].id + '.pem', fp2), + mock.call(self.amp, self.lb.id, + sconts[1].id + '.pem', fp3), + ] + + self.driver.clients[API_VERSION].upload_cert_pem.assert_has_calls( + ucp_calls, any_order=True) + + # upload only one config file + self.driver.clients[API_VERSION].upload_config.assert_called_once_with( + self.amp, self.lb.id, 'fake_config', timeout_dict=None) + # start should be called once + self.driver.clients[ + API_VERSION].reload_listener.assert_called_once_with( + self.amp, self.lb.id, timeout_dict=None) + secret_calls = [ + mock.call(self.sl, self.sl.client_ca_tls_certificate_id, self.amp, + self.lb.id), + mock.call(self.sl, self.sl.client_crl_container_id, self.amp, + self.lb.id) + ] + mock_secret.assert_has_calls(secret_calls) + + def test_udp_update(self): + self.driver.udp_jinja.build_config.side_effect = ['fake_udp_config'] + + # Execute driver method + self.driver.update(self.lb_udp) + + # upload only one config file + self.driver.clients[ + API_VERSION].upload_udp_config.assert_called_once_with( + self.amp, self.sl_udp.id, 'fake_udp_config', timeout_dict=None) + + # start should be called once + self.driver.clients[ + API_VERSION].reload_listener.assert_called_once_with( + self.amp, self.sl_udp.id, timeout_dict=None) + + def test_upload_cert_amp(self): + self.driver.upload_cert_amp(self.amp, six.b('test')) + self.driver.clients[ + API_VERSION].update_cert_for_rotation.assert_called_once_with( + self.amp, six.b('test')) + + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test__process_tls_certificates_no_ca_cert(self, mock_load_crt): + sample_listener = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True) + sconts = [] + for sni_container in sample_listener.sni_containers: + sconts.append(sni_container.tls_container) + mock_load_crt.return_value = { + 'tls_cert': self.sl.default_tls_container, + 'sni_certs': sconts + } + self.driver.clients[API_VERSION].get_cert_md5sum.side_effect = [ + exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] + self.driver._process_tls_certificates( + sample_listener, self.amp, sample_listener.load_balancer.id) + gcm_calls = [ + mock.call(self.amp, self.lb.id, + self.sl.default_tls_container.id + '.pem', + ignore=(404,)), + mock.call(self.amp, self.lb.id, + sconts[0].id + '.pem', ignore=(404,)), + mock.call(self.amp, self.lb.id, + sconts[1].id + '.pem', ignore=(404,)) + ] + self.driver.clients[API_VERSION].get_cert_md5sum.assert_has_calls( + gcm_calls, any_order=True) + fp1 = b'\n'.join([sample_certs.X509_CERT, + sample_certs.X509_CERT_KEY, + sample_certs.X509_IMDS]) + b'\n' + fp2 = b'\n'.join([sample_certs.X509_CERT_2, + sample_certs.X509_CERT_KEY_2, + sample_certs.X509_IMDS]) + b'\n' + fp3 = b'\n'.join([sample_certs.X509_CERT_3, + sample_certs.X509_CERT_KEY_3, + sample_certs.X509_IMDS]) + b'\n' + ucp_calls = [ + mock.call(self.amp, self.lb.id, + self.sl.default_tls_container.id + '.pem', fp1), + mock.call(self.amp, self.lb.id, + sconts[0].id + '.pem', fp2), + mock.call(self.amp, self.lb.id, + sconts[1].id + '.pem', fp3) + ] + self.driver.clients[API_VERSION].upload_cert_pem.assert_has_calls( + ucp_calls, any_order=True) + self.assertEqual( + 3, self.driver.clients[API_VERSION].upload_cert_pem.call_count) + + @mock.patch('oslo_context.context.RequestContext') + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._upload_cert') + def test_process_secret(self, mock_upload_cert, mock_oslo): + # Test bypass if no secret_ref + sample_listener = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True) + + result = self.driver._process_secret(sample_listener, None) + + self.assertIsNone(result) + self.driver.cert_manager.get_secret.assert_not_called() + + # Test the secret process + sample_listener = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True) + fake_context = 'fake context' + fake_secret = b'fake cert' + mock_oslo.return_value = fake_context + self.driver.cert_manager.get_secret.reset_mock() + self.driver.cert_manager.get_secret.return_value = fake_secret + ref_md5 = hashlib.md5(fake_secret).hexdigest() # nosec + ref_id = hashlib.sha1(fake_secret).hexdigest() # nosec + ref_name = '{id}.pem'.format(id=ref_id) + + result = self.driver._process_secret( + sample_listener, sample_listener.client_ca_tls_certificate_id, + self.amp, sample_listener.id) + + mock_oslo.assert_called_once_with( + project_id=sample_listener.project_id) + self.driver.cert_manager.get_secret.assert_called_once_with( + fake_context, sample_listener.client_ca_tls_certificate_id) + mock_upload_cert.assert_called_once_with( + self.amp, sample_listener.id, pem=fake_secret, + md5=ref_md5, name=ref_name) + self.assertEqual(ref_name, result) + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_pool_certs') + def test__process_listener_pool_certs(self, mock_pool_cert): + sample_listener = sample_configs_combined.sample_listener_tuple( + l7=True) + + ref_pool_cert_1 = {'client_cert': '/some/fake/cert-1.pem'} + ref_pool_cert_2 = {'client_cert': '/some/fake/cert-2.pem'} + + mock_pool_cert.side_effect = [ref_pool_cert_1, ref_pool_cert_2] + + ref_cert_dict = {'sample_pool_id_1': ref_pool_cert_1, + 'sample_pool_id_2': ref_pool_cert_2} + + result = self.driver._process_listener_pool_certs( + sample_listener, self.amp, sample_listener.load_balancer.id) + + pool_certs_calls = [ + mock.call(sample_listener, sample_listener.default_pool, + self.amp, sample_listener.load_balancer.id), + mock.call(sample_listener, sample_listener.pools[1], + self.amp, sample_listener.load_balancer.id) + ] + + mock_pool_cert.assert_has_calls(pool_certs_calls, any_order=True) + + self.assertEqual(ref_cert_dict, result) + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_secret') + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._upload_cert') + @mock.patch('octavia.common.tls_utils.cert_parser.build_pem') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test__process_pool_certs(self, mock_load_certs, mock_build_pem, + mock_upload_cert, mock_secret): + fake_cert_dir = '/fake/cert/dir' + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="haproxy_amphora", base_cert_dir=fake_cert_dir) + sample_listener = sample_configs_combined.sample_listener_tuple( + pool_cert=True, pool_ca_cert=True, pool_crl=True) + cert_data_mock = mock.MagicMock() + cert_data_mock.id = uuidutils.generate_uuid() + mock_load_certs.return_value = cert_data_mock + fake_pem = b'fake pem' + mock_build_pem.return_value = fake_pem + ref_md5 = hashlib.md5(fake_pem).hexdigest() # nosec + ref_name = '{id}.pem'.format(id=cert_data_mock.id) + ref_path = '{cert_dir}/{list_id}/{name}'.format( + cert_dir=fake_cert_dir, list_id=sample_listener.id, name=ref_name) + ref_ca_name = 'fake_ca.pem' + ref_ca_path = '{cert_dir}/{list_id}/{name}'.format( + cert_dir=fake_cert_dir, list_id=sample_listener.id, + name=ref_ca_name) + ref_crl_name = 'fake_crl.pem' + ref_crl_path = '{cert_dir}/{list_id}/{name}'.format( + cert_dir=fake_cert_dir, list_id=sample_listener.id, + name=ref_crl_name) + ref_result = {'client_cert': ref_path, 'ca_cert': ref_ca_path, + 'crl': ref_crl_path} + mock_secret.side_effect = [ref_ca_name, ref_crl_name] + + result = self.driver._process_pool_certs( + sample_listener, sample_listener.default_pool, self.amp, + sample_listener.load_balancer.id) + + secret_calls = [ + mock.call(sample_listener, + sample_listener.default_pool.ca_tls_certificate_id, + self.amp, sample_listener.load_balancer.id), + mock.call(sample_listener, + sample_listener.default_pool.crl_container_id, + self.amp, sample_listener.load_balancer.id)] + + mock_build_pem.assert_called_once_with(cert_data_mock) + mock_upload_cert.assert_called_once_with( + self.amp, sample_listener.load_balancer.id, pem=fake_pem, + md5=ref_md5, name=ref_name) + mock_secret.assert_has_calls(secret_calls) + self.assertEqual(ref_result, result) + + def test_start(self): + amp1 = mock.MagicMock() + amp1.api_version = API_VERSION + amp2 = mock.MagicMock() + amp2.api_version = API_VERSION + amp2.status = constants.DELETED + loadbalancer = mock.MagicMock() + loadbalancer.id = uuidutils.generate_uuid() + loadbalancer.amphorae = [amp1, amp2] + loadbalancer.vip = self.sv + listener = mock.MagicMock() + listener.id = uuidutils.generate_uuid() + listener.protocol = constants.PROTOCOL_HTTP + loadbalancer.listeners = [listener] + listener.load_balancer = loadbalancer + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' + # Execute driver method + self.driver.start(loadbalancer) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( + amp1, loadbalancer.id) + + def test_start_with_amphora(self): + # Execute driver method + amp = mock.MagicMock() + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' + self.driver.start(self.lb, self.amp) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( + self.amp, self.lb.id) + + self.driver.clients[API_VERSION].start_listener.reset_mock() + amp.status = constants.DELETED + self.driver.start(self.lb, amp) + self.driver.clients[API_VERSION].start_listener.assert_not_called() + + def test_udp_start(self): + self.driver.clients[ + API_VERSION].start_listener.__name__ = 'start_listener' + # Execute driver method + self.driver.start(self.lb_udp) + self.driver.clients[ + API_VERSION].start_listener.assert_called_once_with( + self.amp, self.sl_udp.id) + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_secret') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + @mock.patch('octavia.common.tls_utils.cert_parser.get_host_names') + def test_delete_second_listener(self, mock_cert, mock_load_crt, + mock_secret): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + sl = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + sl2 = sample_configs_combined.sample_listener_tuple( + id='sample_listener_id_2') + sl.load_balancer.listeners.append(sl2) + mock_cert.return_value = {'cn': sample_certs.X509_CERT_CN} + mock_secret.side_effect = ['filename.pem', 'crl-filename.pem'] + sconts = [] + for sni_container in self.sl.sni_containers: + sconts.append(sni_container.tls_container) + mock_load_crt.side_effect = [{ + 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts}, + {'tls_cert': None, 'sni_certs': []}] + self.driver.jinja_combo.build_config.side_effect = ['fake_config'] + # Execute driver method + self.driver.delete(sl) + + # All of the pem files should be removed + dcp_calls = [ + mock.call(self.amp, sl.load_balancer.id, + self.sl.default_tls_container.id + '.pem'), + mock.call(self.amp, sl.load_balancer.id, sconts[0].id + '.pem'), + mock.call(self.amp, sl.load_balancer.id, sconts[1].id + '.pem'), + ] + self.driver.clients[API_VERSION].delete_cert_pem.assert_has_calls( + dcp_calls, any_order=True) + + # Now just make sure we did an update and not a delete + self.driver.clients[API_VERSION].delete_listener.assert_not_called() + self.driver.clients[API_VERSION].upload_config.assert_called_once_with( + self.amp, sl.load_balancer.id, 'fake_config', timeout_dict=None) + # start should be called once + self.driver.clients[ + API_VERSION].reload_listener.assert_called_once_with( + self.amp, sl.load_balancer.id, timeout_dict=None) + + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_delete_last_listener(self, mock_load_crt): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + sl = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True, client_crl_cert=True, + recursive_nest=True) + mock_load_crt.side_effect = [{ + 'tls_cert': sl.default_tls_container, 'sni_certs': None}] + # Execute driver method + self.driver.delete(sl) + self.driver.clients[ + API_VERSION].delete_listener.assert_called_once_with( + self.amp, sl.load_balancer.id) + + def test_udp_delete(self): + self.driver.clients[ + API_VERSION].delete_listener.__name__ = 'delete_listener' + # Execute driver method + self.driver.delete(self.sl_udp) + self.driver.clients[ + API_VERSION].delete_listener.assert_called_once_with( + self.amp, self.sl_udp.id) + + def test_get_info(self): + expected_info = {'haproxy_version': '1.6.3-1ubuntu0.1', + 'api_version': '1.0'} + result = self.driver.get_info(self.amp) + self.assertEqual(expected_info, result) + + def test_get_diagnostics(self): + # TODO(johnsom) Implement once this exists on the amphora agent. + result = self.driver.get_diagnostics(self.amp) + self.assertIsNone(result) + + def test_finalize_amphora(self): + # TODO(johnsom) Implement once this exists on the amphora agent. + result = self.driver.finalize_amphora(self.amp) + self.assertIsNone(result) + + def test_post_vip_plug(self): + amphorae_network_config = mock.MagicMock() + amphorae_network_config.get().vip_subnet.cidr = FAKE_CIDR + amphorae_network_config.get().vip_subnet.gateway_ip = FAKE_GATEWAY + amphorae_network_config.get().vip_subnet.host_routes = self.host_routes + amphorae_network_config.get().vrrp_port = self.port + self.driver.post_vip_plug(self.amp, self.lb, amphorae_network_config) + self.driver.clients[API_VERSION].plug_vip.assert_called_once_with( + self.amp, self.lb.vip.ip_address, self.subnet_info) + + def test_post_network_plug(self): + # Test dhcp path + port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[], + network=self.network) + self.driver.post_network_plug(self.amp, port) + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[], + mtu=FAKE_MTU)) + + self.driver.clients[API_VERSION].plug_network.reset_mock() + + # Test fixed IP path + self.driver.post_network_plug(self.amp, self.port) + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[dict(ip_address='198.51.100.5', + subnet_cidr='198.51.100.0/24', + host_routes=[])], + mtu=FAKE_MTU)) + + def test_post_network_plug_with_host_routes(self): + SUBNET_ID = 'SUBNET_ID' + FIXED_IP1 = '192.0.2.2' + FIXED_IP2 = '192.0.2.3' + SUBNET_CIDR = '192.0.2.0/24' + DEST1 = '198.51.100.0/24' + DEST2 = '203.0.113.0/24' + NEXTHOP = '192.0.2.1' + host_routes = [network_models.HostRoute(destination=DEST1, + nexthop=NEXTHOP), + network_models.HostRoute(destination=DEST2, + nexthop=NEXTHOP)] + subnet = network_models.Subnet(id=SUBNET_ID, cidr=SUBNET_CIDR, + ip_version=4, host_routes=host_routes) + fixed_ips = [ + network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP1, + subnet=subnet), + network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP2, + subnet=subnet) + ] + port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=fixed_ips, + network=self.network) + self.driver.post_network_plug(self.amp, port) + expected_fixed_ips = [ + {'ip_address': FIXED_IP1, 'subnet_cidr': SUBNET_CIDR, + 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, + {'destination': DEST2, 'nexthop': NEXTHOP}]}, + {'ip_address': FIXED_IP2, 'subnet_cidr': SUBNET_CIDR, + 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, + {'destination': DEST2, 'nexthop': NEXTHOP}]} + ] + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=expected_fixed_ips, + mtu=FAKE_MTU)) + + def test_get_vrrp_interface(self): + self.driver.get_vrrp_interface(self.amp) + self.driver.clients[API_VERSION].get_interface.assert_called_once_with( + self.amp, self.amp.vrrp_ip, timeout_dict=None) + + def test_get_haproxy_versions(self): + ref_haproxy_versions = ['1', '6'] + result = self.driver._get_haproxy_versions(self.amp) + self.driver.clients[API_VERSION].get_info.assert_called_once_with( + self.amp) + self.assertEqual(ref_haproxy_versions, result) + + def test_populate_amphora_api_version(self): + + # Normal path, populate the version + # clear out any previous values + ref_haproxy_version = list(map(int, API_VERSION.split('.'))) + mock_amp = mock.MagicMock() + mock_amp.api_version = None + result = self.driver._populate_amphora_api_version(mock_amp) + self.assertEqual(API_VERSION, mock_amp.api_version) + self.assertEqual(ref_haproxy_version, result) + + # Existing version passed in + fake_version = '9999.9999' + ref_haproxy_version = list(map(int, fake_version.split('.'))) + mock_amp = mock.MagicMock() + mock_amp.api_version = fake_version + result = self.driver._populate_amphora_api_version(mock_amp) + self.assertEqual(fake_version, mock_amp.api_version) + self.assertEqual(ref_haproxy_version, result) + + def test_update_amphora_agent_config(self): + self.driver.update_amphora_agent_config(self.amp, six.b('test')) + self.driver.clients[ + API_VERSION].update_agent_config.assert_called_once_with( + self.amp, six.b('test'), timeout_dict=None) + + +class TestAmphoraAPIClientTest(base.TestCase): + + def setUp(self): + super(TestAmphoraAPIClientTest, self).setUp() + self.driver = driver.AmphoraAPIClient1_0() + self.base_url = "https://192.0.2.77:9443/" + self.base_url_ver = self.base_url + "1.0" + self.amp = models.Amphora(lb_network_ip='192.0.2.77', compute_id='123') + self.amp.api_version = API_VERSION + self.port_info = dict(mac_address=FAKE_MAC_ADDRESS) + # Override with much lower values for testing purposes.. + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="haproxy_amphora", connection_max_retries=2) + + self.subnet_info = {'subnet_cidr': FAKE_CIDR, + 'gateway': FAKE_GATEWAY, + 'mac_address': FAKE_MAC_ADDRESS, + 'vrrp_ip': self.amp.vrrp_ip} + patcher = mock.patch('time.sleep').start() + self.addCleanup(patcher.stop) + self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1, + constants.REQ_READ_TIMEOUT: 2, + constants.CONN_MAX_RETRIES: 3, + constants.CONN_RETRY_INTERVAL: 4} + + def test_base_url(self): + url = self.driver._base_url(FAKE_IP) + self.assertEqual('https://192.0.2.10:9443/', url) + url = self.driver._base_url(FAKE_IPV6, self.amp.api_version) + self.assertEqual('https://[2001:db8::cafe]:9443/1.0/', url) + url = self.driver._base_url(FAKE_IPV6_LLA, self.amp.api_version) + self.assertEqual('https://[fe80::00ff:fe00:cafe%o-hm0]:9443/1.0/', url) + + @mock.patch('requests.Session.get', side_effect=requests.ConnectionError) + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.time.sleep') + def test_request(self, mock_sleep, mock_get): + self.assertRaises(driver_except.TimeOutException, + self.driver.request, 'get', self.amp, + 'unavailableURL', self.timeout_dict) + + @requests_mock.mock() + def test_get_api_version(self, mock_requests): + ref_api_version = {'api_version': '0.1'} + mock_requests.get('{base}/'.format(base=self.base_url), + json=ref_api_version) + result = self.driver.get_api_version(self.amp) + self.assertEqual(ref_api_version, result) + + @requests_mock.mock() + def test_get_info(self, m): + info = {"hostname": "some_hostname", "version": "some_version", + "api_version": "0.5", "uuid": FAKE_UUID_1} + m.get("{base}/info".format(base=self.base_url_ver), + json=info) + information = self.driver.get_info(self.amp) + self.assertEqual(info, information) + + @requests_mock.mock() + def test_get_info_unauthorized(self, m): + m.get("{base}/info".format(base=self.base_url_ver), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.get_info, self.amp) + + @requests_mock.mock() + def test_get_info_missing(self, m): + m.get("{base}/info".format(base=self.base_url_ver), + status_code=404, + headers={'content-type': 'application/json'}) + self.assertRaises(exc.NotFound, self.driver.get_info, self.amp) + + @requests_mock.mock() + def test_get_info_server_error(self, m): + m.get("{base}/info".format(base=self.base_url_ver), + status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.get_info, + self.amp) + + @requests_mock.mock() + def test_get_info_service_unavailable(self, m): + m.get("{base}/info".format(base=self.base_url_ver), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.get_info, + self.amp) + + @requests_mock.mock() + def test_get_details(self, m): + details = {"hostname": "some_hostname", "version": "some_version", + "api_version": "0.5", "uuid": FAKE_UUID_1, + "network_tx": "some_tx", "network_rx": "some_rx", + "active": True, "haproxy_count": 10} + m.get("{base}/details".format(base=self.base_url_ver), + json=details) + amp_details = self.driver.get_details(self.amp) + self.assertEqual(details, amp_details) + + @requests_mock.mock() + def test_get_details_unauthorized(self, m): + m.get("{base}/details".format(base=self.base_url_ver), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.get_details, self.amp) + + @requests_mock.mock() + def test_get_details_missing(self, m): + m.get("{base}/details".format(base=self.base_url_ver), + status_code=404, + headers={'content-type': 'application/json'}) + self.assertRaises(exc.NotFound, self.driver.get_details, self.amp) + + @requests_mock.mock() + def test_get_details_server_error(self, m): + m.get("{base}/details".format(base=self.base_url_ver), + status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.get_details, + self.amp) + + @requests_mock.mock() + def test_get_details_service_unavailable(self, m): + m.get("{base}/details".format(base=self.base_url_ver), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.get_details, + self.amp) + + @requests_mock.mock() + def test_get_all_listeners(self, m): + listeners = [{"status": "ONLINE", "provisioning_status": "ACTIVE", + "type": "PASSIVE", "uuid": FAKE_UUID_1}] + m.get("{base}/listeners".format(base=self.base_url_ver), + json=listeners) + all_listeners = self.driver.get_all_listeners(self.amp) + self.assertEqual(listeners, all_listeners) + + @requests_mock.mock() + def test_get_all_listeners_unauthorized(self, m): + m.get("{base}/listeners".format(base=self.base_url_ver), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.get_all_listeners, + self.amp) + + @requests_mock.mock() + def test_get_all_listeners_missing(self, m): + m.get("{base}/listeners".format(base=self.base_url_ver), + status_code=404, + headers={'content-type': 'application/json'}) + self.assertRaises(exc.NotFound, self.driver.get_all_listeners, + self.amp) + + @requests_mock.mock() + def test_get_all_listeners_server_error(self, m): + m.get("{base}/listeners".format(base=self.base_url_ver), + status_code=500) + self.assertRaises(exc.InternalServerError, + self.driver.get_all_listeners, self.amp) + + @requests_mock.mock() + def test_get_all_listeners_service_unavailable(self, m): + m.get("{base}/listeners".format(base=self.base_url_ver), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, + self.driver.get_all_listeners, self.amp) + + @requests_mock.mock() + def test_start_loadbalancer(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/start".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1)) + self.driver.start_listener(self.amp, FAKE_UUID_1) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_start_loadbalancer_missing(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/start".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1), + status_code=404, + headers={'content-type': 'application/json'}) + self.assertRaises(exc.NotFound, self.driver.start_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_start_loadbalancer_unauthorized(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/start".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.start_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_start_loadbalancer_server_error(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/start".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1), + status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.start_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_start_loadbalancer_service_unavailable(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/start".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.start_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_delete_listener(self, m): + m.delete("{base}/listeners/{listener_id}".format( + base=self.base_url_ver, listener_id=FAKE_UUID_1), json={}) + self.driver.delete_listener(self.amp, FAKE_UUID_1) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_delete_listener_missing(self, m): + m.delete("{base}/listeners/{listener_id}".format( + base=self.base_url_ver, listener_id=FAKE_UUID_1), + status_code=404, + headers={'content-type': 'application/json'}) + self.driver.delete_listener(self.amp, FAKE_UUID_1) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_delete_listener_unauthorized(self, m): + m.delete("{base}/listeners/{listener_id}".format( + base=self.base_url_ver, listener_id=FAKE_UUID_1), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.delete_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_delete_listener_server_error(self, m): + m.delete("{base}/listeners/{listener_id}".format( + base=self.base_url_ver, listener_id=FAKE_UUID_1), + status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.delete_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_delete_listener_service_unavailable(self, m): + m.delete("{base}/listeners/{listener_id}".format( + base=self.base_url_ver, listener_id=FAKE_UUID_1), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.delete_listener, + self.amp, FAKE_UUID_1) + + @requests_mock.mock() + def test_upload_cert_pem(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME)) + self.driver.upload_cert_pem(self.amp, FAKE_UUID_1, + FAKE_PEM_FILENAME, + "some_file") + self.assertTrue(m.called) + + @requests_mock.mock() + def test_upload_invalid_cert_pem(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=400) + self.assertRaises(exc.InvalidRequest, self.driver.upload_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, + "some_file") + + @requests_mock.mock() + def test_upload_cert_pem_unauthorized(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.upload_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, + "some_file") + + @requests_mock.mock() + def test_upload_cert_pem_server_error(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.upload_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, + "some_file") + + @requests_mock.mock() + def test_upload_cert_pem_service_unavailable(self, m): + m.put("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.upload_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME, + "some_file") + + @requests_mock.mock() + def test_update_cert_for_rotation(self, m): + m.put("{base}/certificate".format(base=self.base_url_ver)) + resp_body = self.driver.update_cert_for_rotation(self.amp, + "some_file") + self.assertEqual(200, resp_body.status_code) + + @requests_mock.mock() + def test_update_invalid_cert_for_rotation(self, m): + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=400) + self.assertRaises(exc.InvalidRequest, + self.driver.update_cert_for_rotation, self.amp, + "some_file") + + @requests_mock.mock() + def test_update_cert_for_rotation_unauthorized(self, m): + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=401) + self.assertRaises(exc.Unauthorized, + self.driver.update_cert_for_rotation, self.amp, + "some_file") + + @requests_mock.mock() + def test_update_cert_for_rotation_error(self, m): + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=500) + self.assertRaises(exc.InternalServerError, + self.driver.update_cert_for_rotation, self.amp, + "some_file") + + @requests_mock.mock() + def test_update_cert_for_rotation_unavailable(self, m): + m.put("{base}/certificate".format(base=self.base_url_ver), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, + self.driver.update_cert_for_rotation, self.amp, + "some_file") + + @requests_mock.mock() + def test_get_cert_5sum(self, m): + md5sum = {"md5sum": "some_real_sum"} + m.get("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), json=md5sum) + sum_test = self.driver.get_cert_md5sum(self.amp, FAKE_UUID_1, + FAKE_PEM_FILENAME) + self.assertIsNotNone(sum_test) + + @requests_mock.mock() + def test_get_cert_5sum_missing(self, m): + m.get("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=404, + headers={'content-type': 'application/json'}) + self.assertRaises(exc.NotFound, self.driver.get_cert_md5sum, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_get_cert_5sum_unauthorized(self, m): + m.get("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.get_cert_md5sum, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_get_cert_5sum_server_error(self, m): + m.get("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.get_cert_md5sum, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_get_cert_5sum_service_unavailable(self, m): + m.get("{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.get_cert_md5sum, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_delete_cert_pem(self, m): + m.delete( + "{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME)) + self.driver.delete_cert_pem(self.amp, FAKE_UUID_1, + FAKE_PEM_FILENAME) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_delete_cert_pem_missing(self, m): + m.delete( + "{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=404, + headers={'content-type': 'application/json'}) + self.driver.delete_cert_pem(self.amp, FAKE_UUID_1, + FAKE_PEM_FILENAME) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_delete_cert_pem_unauthorized(self, m): + m.delete( + "{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.delete_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_delete_cert_pem_server_error(self, m): + m.delete( + "{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.delete_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_delete_cert_pem_service_unavailable(self, m): + m.delete( + "{base}/loadbalancer/{loadbalancer_id}/certificates/" + "{filename}".format( + base=self.base_url_ver, loadbalancer_id=FAKE_UUID_1, + filename=FAKE_PEM_FILENAME), status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.delete_cert_pem, + self.amp, FAKE_UUID_1, FAKE_PEM_FILENAME) + + @requests_mock.mock() + def test_upload_config(self, m): + config = {"name": "fake_config"} + m.put( + "{base}/loadbalancer/{" + "amphora_id}/{loadbalancer_id}/haproxy".format( + amphora_id=self.amp.id, base=self.base_url_ver, + loadbalancer_id=FAKE_UUID_1), + json=config) + self.driver.upload_config(self.amp, FAKE_UUID_1, + config) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_upload_invalid_config(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/loadbalancer/{" + "amphora_id}/{loadbalancer_id}/haproxy".format( + amphora_id=self.amp.id, base=self.base_url_ver, + loadbalancer_id=FAKE_UUID_1), + status_code=400) + self.assertRaises(exc.InvalidRequest, self.driver.upload_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_config_unauthorized(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/loadbalancer/{" + "amphora_id}/{loadbalancer_id}/haproxy".format( + amphora_id=self.amp.id, base=self.base_url_ver, + loadbalancer_id=FAKE_UUID_1), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.upload_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_config_server_error(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/loadbalancer/{" + "amphora_id}/{loadbalancer_id}/haproxy".format( + amphora_id=self.amp.id, base=self.base_url_ver, + loadbalancer_id=FAKE_UUID_1), + status_code=500) + self.assertRaises(exc.InternalServerError, self.driver.upload_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_config_service_unavailable(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/loadbalancer/{" + "amphora_id}/{loadbalancer_id}/haproxy".format( + amphora_id=self.amp.id, base=self.base_url_ver, + loadbalancer_id=FAKE_UUID_1), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, self.driver.upload_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config(self, m): + config = {"name": "fake_config"} + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url_ver, + listener_id=FAKE_UUID_1), + json=config) + self.driver.upload_udp_config(self.amp, FAKE_UUID_1, config) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_upload_udp_invalid_config(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url_ver, + listener_id=FAKE_UUID_1), + status_code=400) + self.assertRaises(exc.InvalidRequest, self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_unauthorized(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url_ver, + listener_id=FAKE_UUID_1), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_server_error(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url_ver, + listener_id=FAKE_UUID_1), + status_code=500) + self.assertRaises(exc.InternalServerError, + self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_service_unavailable(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url_ver, + listener_id=FAKE_UUID_1), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, + self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_plug_vip(self, m): + m.post("{base}/plug/vip/{vip}".format( + base=self.base_url_ver, vip=FAKE_IP) + ) + self.driver.plug_vip(self.amp, FAKE_IP, self.subnet_info) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_plug_vip_api_not_ready(self, m): + m.post("{base}/plug/vip/{vip}".format( + base=self.base_url_ver, vip=FAKE_IP), + status_code=404, headers={'content-type': 'text/html'} + ) + self.assertRaises(driver_except.TimeOutException, + self.driver.plug_vip, + self.amp, FAKE_IP, self.subnet_info) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_plug_network(self, m): + m.post("{base}/plug/network".format( + base=self.base_url_ver) + ) + self.driver.plug_network(self.amp, self.port_info) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_upload_vrrp_config(self, m): + config = '{"name": "bad_config"}' + m.put("{base}/vrrp/upload".format( + base=self.base_url_ver) + ) + self.driver.upload_vrrp_config(self.amp, config) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_vrrp_action(self, m): + action = 'start' + m.put("{base}/vrrp/{action}".format(base=self.base_url_ver, + action=action)) + self.driver._vrrp_action(action, self.amp) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_get_interface(self, m): + interface = [{"interface": "eth1"}] + ip_addr = '192.51.100.1' + m.get("{base}/interface/{ip_addr}".format(base=self.base_url_ver, + ip_addr=ip_addr), + json=interface) + self.driver.get_interface(self.amp, ip_addr) + self.assertTrue(m.called) + + m.register_uri('GET', + self.base_url_ver + '/interface/' + ip_addr, + status_code=500, reason='FAIL', json='FAIL') + self.assertRaises(exc.InternalServerError, + self.driver.get_interface, + self.amp, ip_addr) + + @requests_mock.mock() + def test_update_agent_config(self, m): + m.put("{base}/config".format(base=self.base_url_ver)) + resp_body = self.driver.update_agent_config(self.amp, "some_file") + self.assertEqual(200, resp_body.status_code) + + @requests_mock.mock() + def test_update_agent_config_error(self, m): + m.put("{base}/config".format(base=self.base_url_ver), status_code=500) + self.assertRaises(exc.InternalServerError, + self.driver.update_agent_config, self.amp, + "some_file") diff --git a/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py b/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py index 2512262202..9b34c2507e 100644 --- a/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py +++ b/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py @@ -20,18 +20,25 @@ from octavia.amphorae.drivers.keepalived import vrrp_rest_driver from octavia.common import constants import octavia.tests.unit.base as base +# Version 1.0 is functionally identical to all versions before it +API_VERSION = '1.0' + class TestVRRPRestDriver(base.TestCase): def setUp(self): self.keepalived_mixin = vrrp_rest_driver.KeepalivedAmphoraDriverMixin() - self.keepalived_mixin.client = mock.MagicMock() - self.client = self.keepalived_mixin.client + self.keepalived_mixin.clients = { + 'base': mock.MagicMock(), + API_VERSION: mock.MagicMock()} + self.keepalived_mixin._populate_amphora_api_version = mock.MagicMock() + self.clients = self.keepalived_mixin.clients self.FAKE_CONFIG = 'FAKE CONFIG' self.lb_mock = mock.MagicMock() self.amphora_mock = mock.MagicMock() self.amphora_mock.id = uuidutils.generate_uuid() self.amphora_mock.status = constants.AMPHORA_ALLOCATED + self.amphora_mock.api_version = API_VERSION self.lb_mock.amphorae = [self.amphora_mock] self.amphorae_network_config = {} vip_subnet = mock.MagicMock() @@ -49,7 +56,7 @@ class TestVRRPRestDriver(base.TestCase): self.keepalived_mixin.update_vrrp_conf(self.lb_mock, self.amphorae_network_config) - self.client.upload_vrrp_config.assert_called_once_with( + self.clients[API_VERSION].upload_vrrp_config.assert_called_once_with( self.amphora_mock, self.FAKE_CONFIG) @@ -57,16 +64,19 @@ class TestVRRPRestDriver(base.TestCase): self.keepalived_mixin.stop_vrrp_service(self.lb_mock) - self.client.stop_vrrp.assert_called_once_with(self.amphora_mock) + self.clients[API_VERSION].stop_vrrp.assert_called_once_with( + self.amphora_mock) def test_start_vrrp_service(self): self.keepalived_mixin.start_vrrp_service(self.lb_mock) - self.client.start_vrrp.assert_called_once_with(self.amphora_mock) + self.clients[API_VERSION].start_vrrp.assert_called_once_with( + self.amphora_mock) def test_reload_vrrp_service(self): self.keepalived_mixin.reload_vrrp_service(self.lb_mock) - self.client.reload_vrrp.assert_called_once_with(self.amphora_mock) + self.clients[API_VERSION].reload_vrrp.assert_called_once_with( + self.amphora_mock) diff --git a/octavia/tests/unit/amphorae/drivers/test_noop_amphoraloadbalancer_driver.py b/octavia/tests/unit/amphorae/drivers/test_noop_amphoraloadbalancer_driver.py index 2ce085679b..256a27aed8 100644 --- a/octavia/tests/unit/amphorae/drivers/test_noop_amphoraloadbalancer_driver.py +++ b/octavia/tests/unit/amphorae/drivers/test_noop_amphoraloadbalancer_driver.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from oslo_utils import uuidutils from octavia.amphorae.drivers.noop_driver import driver @@ -54,6 +55,7 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase): self.load_balancer = data_models.LoadBalancer( id=FAKE_UUID_1, amphorae=[self.amphora], vip=self.vip, listeners=[self.listener]) + self.listener.load_balancer = self.load_balancer self.network = network_models.Network(id=self.FAKE_UUID_1) self.port = network_models.Port(id=uuidutils.generate_uuid()) self.amphorae_net_configs = { @@ -70,8 +72,7 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase): constants.CONN_RETRY_INTERVAL: 4} def test_update_amphora_listeners(self): - amphorae = [self.amphora] - self.driver.update_amphora_listeners([self.listener], 0, amphorae, + self.driver.update_amphora_listeners(self.load_balancer, self.amphora, self.timeout_dict) self.assertEqual((self.listener, self.amphora.id, self.timeout_dict, 'update_amp'), @@ -80,28 +81,22 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase): self.amphora.id)]) def test_update(self): - self.driver.update(self.listener, self.vip) - self.assertEqual((self.listener, self.vip, 'active'), + self.driver.update(self.load_balancer) + self.assertEqual(([self.listener], self.vip, 'active'), self.driver.driver.amphoraconfig[( - self.listener.protocol_port, - self.vip.ip_address)]) - - def test_stop(self): - self.driver.stop(self.listener, self.vip) - self.assertEqual((self.listener, self.vip, 'stop'), - self.driver.driver.amphoraconfig[( - self.listener.protocol_port, + (self.listener.protocol_port,), self.vip.ip_address)]) def test_start(self): - self.driver.start(self.listener, self.vip, amphora='amp1') - self.assertEqual((self.listener, self.vip, 'amp1', 'start'), + mock_amphora = mock.MagicMock() + mock_amphora.id = '321' + self.driver.start(self.load_balancer, amphora=mock_amphora) + self.assertEqual((self.load_balancer, mock_amphora, 'start'), self.driver.driver.amphoraconfig[( - self.listener.protocol_port, - self.vip.ip_address, 'amp1')]) + self.load_balancer.id, '321')]) def test_delete(self): - self.driver.delete(self.listener, self.vip) + self.driver.delete(self.listener) self.assertEqual((self.listener, self.vip, 'delete'), self.driver.driver.amphoraconfig[( self.listener.protocol_port, diff --git a/octavia/tests/unit/certificates/manager/test_barbican.py b/octavia/tests/unit/certificates/manager/test_barbican.py index f56fa0fe87..8945a16c70 100644 --- a/octavia/tests/unit/certificates/manager/test_barbican.py +++ b/octavia/tests/unit/certificates/manager/test_barbican.py @@ -134,7 +134,8 @@ class TestBarbicanManager(base.TestCase): self.assertIsInstance(data, cert.Cert) self.assertEqual(sample.X509_CERT_KEY, data.get_private_key()) self.assertEqual(sample.X509_CERT, data.get_certificate()) - self.assertItemsEqual(sample.X509_IMDS_LIST, data.get_intermediates()) + self.assertEqual(sorted(sample.X509_IMDS_LIST), + sorted(data.get_intermediates())) self.assertIsNone(data.get_private_key_passphrase()) def test_delete_cert_legacy(self): diff --git a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/__init__.py b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py new file mode 100644 index 0000000000..3058401ea1 --- /dev/null +++ b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py @@ -0,0 +1,1205 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import os + +from octavia.common import constants +from octavia.common.jinja.haproxy.combined_listeners import jinja_cfg +from octavia.tests.unit import base +from octavia.tests.unit.common.sample_configs import sample_configs_combined + + +class TestHaproxyCfg(base.TestCase): + def setUp(self): + super(TestHaproxyCfg, self).setUp() + self.jinja_cfg = jinja_cfg.JinjaTemplater( + base_amp_path='/var/lib/octavia', + base_crt_dir='/var/lib/octavia/certs') + + def test_get_template(self): + template = self.jinja_cfg._get_template() + self.assertEqual('haproxy.cfg.j2', template.name) + + def test_render_template_tls(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " redirect scheme https if !{{ ssl_fc }}\n" + " bind 10.0.0.2:443 " + "ssl crt /var/lib/octavia/certs/" + "sample_loadbalancer_id_1/tls_container_id.pem " + "crt /var/lib/octavia/certs/sample_loadbalancer_id_1 " + "ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/" + "client_ca.pem verify required crl-file /var/lib/octavia/" + "certs/sample_loadbalancer_id_1/SHA_ID.pem\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 cookie " + "sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + tls_tupe = sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='imaCert1', private_key='imaPrivateKey1', + primary_cn='FakeCN') + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, sni=True, + client_ca_cert=True, client_crl_cert=True)], + tls_tupe, client_ca_filename='client_ca.pem', + client_crl='SHA_ID.pem') + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + + def test_render_template_tls_no_sni(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " redirect scheme https if !{{ ssl_fc }}\n" + " bind 10.0.0.2:443 " + "ssl crt /var/lib/octavia/certs/" + "sample_loadbalancer_id_1/tls_container_id.pem\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True)], + tls_cert=sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='ImAalsdkfjCert', + private_key='ImAsdlfksdjPrivateKey', + primary_cn="FakeCN")) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + + def test_render_template_http(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple()]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_member_backup(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "addr 192.168.1.1 port 9000 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "addr 192.168.1.1 port 9000 " + "cookie sample_member_id_2 backup\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + monitor_ip_port=True, backup_member=True)]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_custom_timeouts(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:80\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 1\n" + " timeout server 3\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie " + "sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + timeout_member_connect=1, timeout_client_data=2, + timeout_member_data=3)]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + + def test_render_template_null_timeouts(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:80\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie " + "sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + timeout_member_connect=None, timeout_client_data=None, + timeout_member_data=None)]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + + def test_render_template_member_monitor_addr_port(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "addr 192.168.1.1 port 9000 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "addr 192.168.1.1 port 9000 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + monitor_ip_port=True)]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_https_real_monitor(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode tcp\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check check-ssl verify none inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check check-ssl verify none inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(proto='HTTPS')]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_https_hello_monitor(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode tcp\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option ssl-hello-chk\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTPS', monitor_proto='TLS-HELLO')]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_no_monitor_http(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(proto='HTTP', + monitor=False)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + backend=be), rendered_obj) + + def test_render_template_disabled_member(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "cookie sample_member_id_2 disabled\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTP', monitor=False, disabled_member=True)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + backend=be), rendered_obj) + + def test_render_template_ping_monitor_http(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option external-check\n" + " external-check command /var/lib/octavia/ping-wrapper.sh\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + go = " maxconn {maxconn}\n external-check\n\n".format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTP', monitor_proto='PING')]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + backend=be, global_opts=go), rendered_obj) + + def test_render_template_no_monitor_https(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode tcp\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(proto='HTTPS', + monitor=False)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_health_monitor_http_check(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.1\\r\\nHost:\\ " + "testlab.com\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTP', monitor_proto='HTTP', hm_host_http_check=True)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + backend=be), rendered_obj) + + def test_render_template_no_persistence_https(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode tcp\n" + " balance roundrobin\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13\n\n").format(maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTPS', monitor=False, persistence=False)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_no_persistence_http(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13\n\n").format(maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTP', monitor=False, persistence=False)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + backend=be), rendered_obj) + + def test_render_template_sourceip_persistence(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " stick-table type ip size 10k\n" + " stick on src\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + persistence_type='SOURCE_IP')]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_appcookie_persistence(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " stick-table type string len 64 size 10k\n" + " stick store-response res.cook(JSESSIONID)\n" + " stick match req.cook(JSESSIONID)\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + persistence_type='APP_COOKIE', + persistence_cookie='JSESSIONID')]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_unlimited_connections(self): + sample_amphora = sample_configs_combined.sample_amphora_tuple() + sample_listener = sample_configs_combined.sample_listener_tuple( + proto='HTTPS', monitor=False) + fe = ("frontend {listener_id}\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend {pool_id}:{listener_id}\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + pool_id=sample_listener.default_pool.id, + listener_id=sample_listener.id) + be = ("backend {pool_id}:{listener_id}\n" + " mode tcp\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + pool_id=sample_listener.default_pool.id, + listener_id=sample_listener.id) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_amphora, + [sample_listener]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_limited_connections(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ -\\ -\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{+Q}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn 2014\n" + " bind 10.0.0.2:443\n" + " mode tcp\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n") + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode tcp\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " fullconn 2014\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "cookie sample_member_id_2\n\n") + g_opts = " maxconn 2014\n\n" + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='HTTPS', monitor=False, connection_limit=2014)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be, global_opts=g_opts), rendered_obj) + + def test_render_template_l7policies(self): + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:80\n" + " mode http\n" + " acl sample_l7rule_id_1 path -m beg /api\n" + " use_backend sample_pool_id_2:sample_listener_id_1" + " if sample_l7rule_id_1\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect code 302 location http://www.example.com if " + "!sample_l7rule_id_2 sample_l7rule_id_3\n" + " acl sample_l7rule_id_4 path_end -m str jpg\n" + " acl sample_l7rule_id_5 req.hdr(host) -i -m end " + ".example.com\n" + " http-request deny if sample_l7rule_id_4 " + "sample_l7rule_id_5\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect code 302 prefix https://example.com if " + "!sample_l7rule_id_2 sample_l7rule_id_3\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_2\n" + "\n" + "backend sample_pool_id_2:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /healthmon.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_3 10.0.0.97:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(l7=True)]) + self.assertEqual(sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) + + def test_render_template_http_xff(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " option forwardfor\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + insert_headers={'X-Forwarded-For': 'true'})]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_http_xff_xfport(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " option forwardfor\n" + " http-request set-header X-Forwarded-Port %[dst_port]\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + insert_headers={'X-Forwarded-For': 'true', + 'X-Forwarded-Port': 'true'})]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_pool_proxy_protocol(self): + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1 send-proxy\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2 send-proxy\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple(be_proto='PROXY')]) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_pool_cert(self): + cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'sample_listener_id_1', 'fake path') + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1 " + "{opts}\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " + "{opts}\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + pool_cert=True, tls_enabled=True)], + pool_tls_certs={ + 'sample_pool_id_1': + {'client_cert': cert_file_path, + 'ca_cert': None, 'crl': None}}) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_render_template_with_full_pool_cert(self): + pool_client_cert = '/foo/cert.pem' + pool_ca_cert = '/foo/ca.pem' + pool_crl = '/foo/crl.pem' + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1 " + "{opts}\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " + "{opts}\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + opts="%s %s %s %s %s %s" % ( + "ssl", "crt", pool_client_cert, + "ca-file %s" % pool_ca_cert, + "crl-file %s" % pool_crl, + "verify required sni ssl_fc_sni")) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + pool_cert=True, pool_ca_cert=True, pool_crl=True, + tls_enabled=True)], + pool_tls_certs={ + 'sample_pool_id_1': + {'client_cert': pool_client_cert, + 'ca_cert': pool_ca_cert, + 'crl': pool_crl}}) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_transform_session_persistence(self): + in_persistence = ( + sample_configs_combined.sample_session_persistence_tuple()) + ret = self.jinja_cfg._transform_session_persistence( + in_persistence, {}) + self.assertEqual(sample_configs_combined.RET_PERSISTENCE, ret) + + def test_transform_health_monitor(self): + in_persistence = sample_configs_combined.sample_health_monitor_tuple() + ret = self.jinja_cfg._transform_health_monitor(in_persistence, {}) + self.assertEqual(sample_configs_combined.RET_MONITOR_1, ret) + + def test_transform_member(self): + in_member = sample_configs_combined.sample_member_tuple( + 'sample_member_id_1', '10.0.0.99') + ret = self.jinja_cfg._transform_member(in_member, {}) + self.assertEqual(sample_configs_combined.RET_MEMBER_1, ret) + + def test_transform_pool(self): + in_pool = sample_configs_combined.sample_pool_tuple() + ret = self.jinja_cfg._transform_pool(in_pool, {}) + self.assertEqual(sample_configs_combined.RET_POOL_1, ret) + + def test_transform_pool_2(self): + in_pool = sample_configs_combined.sample_pool_tuple(sample_pool=2) + ret = self.jinja_cfg._transform_pool(in_pool, {}) + self.assertEqual(sample_configs_combined.RET_POOL_2, ret) + + def test_transform_pool_http_reuse(self): + in_pool = sample_configs_combined.sample_pool_tuple(sample_pool=2) + ret = self.jinja_cfg._transform_pool( + in_pool, {constants.HTTP_REUSE: True}) + expected_config = copy.copy(sample_configs_combined.RET_POOL_2) + expected_config[constants.HTTP_REUSE] = True + self.assertEqual(expected_config, ret) + + def test_transform_pool_cert(self): + in_pool = sample_configs_combined.sample_pool_tuple(pool_cert=True) + cert_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'test_listener_id', 'pool_cert.pem') + ret = self.jinja_cfg._transform_pool( + in_pool, {}, pool_tls_certs={'client_cert': cert_path}) + expected_config = copy.copy(sample_configs_combined.RET_POOL_1) + expected_config['client_cert'] = cert_path + self.assertEqual(expected_config, ret) + + def test_transform_listener(self): + in_listener = sample_configs_combined.sample_listener_tuple() + ret = self.jinja_cfg._transform_listener(in_listener, None, {}, + in_listener.load_balancer) + self.assertEqual(sample_configs_combined.RET_LISTENER, ret) + + def test_transform_listener_with_l7(self): + in_listener = sample_configs_combined.sample_listener_tuple(l7=True) + ret = self.jinja_cfg._transform_listener(in_listener, None, {}, + in_listener.load_balancer) + self.assertEqual(sample_configs_combined.RET_LISTENER_L7, ret) + + def test_transform_loadbalancer(self): + in_amphora = sample_configs_combined.sample_amphora_tuple() + in_listener = sample_configs_combined.sample_listener_tuple() + ret = self.jinja_cfg._transform_loadbalancer( + in_amphora, in_listener.load_balancer, [in_listener], None, {}) + self.assertEqual(sample_configs_combined.RET_LB, ret) + + def test_transform_amphora(self): + in_amphora = sample_configs_combined.sample_amphora_tuple() + ret = self.jinja_cfg._transform_amphora(in_amphora, {}) + self.assertEqual(sample_configs_combined.RET_AMPHORA, ret) + + def test_transform_loadbalancer_with_l7(self): + in_amphora = sample_configs_combined.sample_amphora_tuple() + in_listener = sample_configs_combined.sample_listener_tuple(l7=True) + ret = self.jinja_cfg._transform_loadbalancer( + in_amphora, in_listener.load_balancer, [in_listener], None, {}) + self.assertEqual(sample_configs_combined.RET_LB_L7, ret) + + def test_transform_l7policy(self): + in_l7policy = sample_configs_combined.sample_l7policy_tuple( + 'sample_l7policy_id_1') + ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) + self.assertEqual(sample_configs_combined.RET_L7POLICY_1, ret) + + def test_transform_l7policy_2_8(self): + in_l7policy = sample_configs_combined.sample_l7policy_tuple( + 'sample_l7policy_id_2', sample_policy=2) + ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) + self.assertEqual(sample_configs_combined.RET_L7POLICY_2, ret) + + # test invalid action without redirect_http_code + in_l7policy = sample_configs_combined.sample_l7policy_tuple( + 'sample_l7policy_id_8', sample_policy=2, redirect_http_code=None) + ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) + self.assertEqual(sample_configs_combined.RET_L7POLICY_8, ret) + + def test_transform_l7policy_disabled_rule(self): + in_l7policy = sample_configs_combined.sample_l7policy_tuple( + 'sample_l7policy_id_6', sample_policy=6) + ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) + self.assertEqual(sample_configs_combined.RET_L7POLICY_6, ret) + + def test_escape_haproxy_config_string(self): + self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( + 'string_with_none'), 'string_with_none') + self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( + 'string with spaces'), 'string\\ with\\ spaces') + self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( + 'string\\with\\backslashes'), 'string\\\\with\\\\backslashes') + self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( + 'string\\ with\\ all'), 'string\\\\\\ with\\\\\\ all') + + def test_expand_expected_codes(self): + exp_codes = '' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + []) + exp_codes = '200' + self.assertEqual( + self.jinja_cfg._expand_expected_codes(exp_codes), ['200']) + exp_codes = '200, 201' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201']) + exp_codes = '200, 201,202' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201', '202']) + exp_codes = '200-202' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201', '202']) + exp_codes = '200-202, 205' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201', '202', '205']) + exp_codes = '200, 201-203' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201', '202', '203']) + exp_codes = '200, 201-203, 205' + self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), + ['200', '201', '202', '203', '205']) + exp_codes = '201-200, 205' + self.assertEqual( + self.jinja_cfg._expand_expected_codes(exp_codes), ['205']) + + def test_render_template_no_log(self): + j_cfg = jinja_cfg.JinjaTemplater( + base_amp_path='/var/lib/octavia', + base_crt_dir='/var/lib/octavia/certs', + connection_logging=False) + defaults = ("defaults\n" + " no log\n" + " retries 3\n" + " option redispatch\n" + " option splice-request\n" + " option splice-response\n" + " option http-keep-alive\n\n") + rendered_obj = j_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple()] + ) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + defaults=defaults), + rendered_obj) + + def test_http_reuse(self): + j_cfg = jinja_cfg.JinjaTemplater( + base_amp_path='/var/lib/octavia', + base_crt_dir='/var/lib/octavia/certs') + + sample_amphora = sample_configs_combined.sample_amphora_tuple() + sample_proxy_listener = sample_configs_combined.sample_listener_tuple( + be_proto='PROXY') + # With http-reuse + be = ("backend {pool_id}:{listener_id}\n" + " mode http\n" + " http-reuse safe\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1 send-proxy\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2 send-proxy\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + pool_id=sample_proxy_listener.default_pool.id, + listener_id=sample_proxy_listener.id) + rendered_obj = j_cfg.build_config( + sample_amphora, + [sample_proxy_listener], + tls_cert=None, + haproxy_versions=("1", "8", "1")) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + # Without http-reuse + be = ("backend {pool_id}:{listener_id}\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_1 send-proxy\n" + " server sample_member_id_2 10.0.0.98:82 " + "weight 13 check inter 30s fall 3 rise 2 " + "cookie sample_member_id_2 send-proxy\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + pool_id=sample_proxy_listener.default_pool.id, + listener_id=sample_proxy_listener.id) + rendered_obj = j_cfg.build_config( + sample_amphora, + [sample_proxy_listener], + tls_cert=None, + haproxy_versions=("1", "5", "18")) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + + def test_ssl_types_l7rules(self): + j_cfg = jinja_cfg.JinjaTemplater( + base_amp_path='/var/lib/octavia', + base_crt_dir='/var/lib/octavia/certs') + fe = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{+Q}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{+Q}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn 1000000\n" + " redirect scheme https if !{ ssl_fc }\n" + " bind 10.0.0.2:443\n" + " mode http\n" + " acl sample_l7rule_id_1 path -m beg /api\n" + " use_backend sample_pool_id_2:sample_listener_id_1" + " if sample_l7rule_id_1\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect code 302 location http://www.example.com " + "if !sample_l7rule_id_2 sample_l7rule_id_3\n" + " acl sample_l7rule_id_4 path_end -m str jpg\n" + " acl sample_l7rule_id_5 req.hdr(host) -i -m end " + ".example.com\n" + " http-request deny " + "if sample_l7rule_id_4 sample_l7rule_id_5\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect code 302 prefix https://example.com " + "if !sample_l7rule_id_2 sample_l7rule_id_3\n" + " acl sample_l7rule_id_7 ssl_c_used\n" + " acl sample_l7rule_id_8 ssl_c_verify eq 1\n" + " acl sample_l7rule_id_9 ssl_c_s_dn(STREET) -m reg " + "^STREET.*NO\\\\.$\n" + " acl sample_l7rule_id_10 ssl_c_s_dn(OU-3) -m beg " + "Orgnization\\ Bala\n" + " acl sample_l7rule_id_11 path -m beg /api\n" + " redirect code 302 location " + "http://www.ssl-type-l7rule-test.com " + "if sample_l7rule_id_7 !sample_l7rule_id_8 !sample_l7rule_id_9 " + "!sample_l7rule_id_10 sample_l7rule_id_11\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n\n") + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn 1000000\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_2\n\n" + "backend sample_pool_id_2:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /healthmon.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn 1000000\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_3 10.0.0.97:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n") + sample_listener = sample_configs_combined.sample_listener_tuple( + proto=constants.PROTOCOL_TERMINATED_HTTPS, l7=True, + ssl_type_l7=True) + rendered_obj = j_cfg.build_config( + sample_configs_combined.sample_amphora_tuple(), + [sample_listener], + tls_cert=None, + haproxy_versions=("1", "5", "18")) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) diff --git a/octavia/tests/unit/common/jinja/haproxy/split_listeners/__init__.py b/octavia/tests/unit/common/jinja/haproxy/split_listeners/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py similarity index 83% rename from octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py rename to octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py index 10edb4f142..623190646d 100644 --- a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py @@ -17,9 +17,9 @@ import copy import os from octavia.common import constants -from octavia.common.jinja.haproxy import jinja_cfg +from octavia.common.jinja.haproxy.split_listeners import jinja_cfg from octavia.tests.unit import base -from octavia.tests.unit.common.sample_configs import sample_configs +from octavia.tests.unit.common.sample_configs import sample_configs_split class TestHaproxyCfg(base.TestCase): @@ -70,20 +70,19 @@ class TestHaproxyCfg(base.TestCase): "weight 13 check inter 30s fall 3 rise 2 cookie " "sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) - tls_tupe = sample_configs.sample_tls_container_tuple( + tls_tupe = sample_configs_split.sample_tls_container_tuple( id='tls_container_id', certificate='imaCert1', private_key='imaPrivateKey1', primary_cn='FakeCN') rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='TERMINATED_HTTPS', - tls=True, sni=True, - client_ca_cert=True, - client_crl_cert=True), + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, sni=True, + client_ca_cert=True, client_crl_cert=True), tls_tupe, client_ca_filename='client_ca.pem', client_crl='SHA_ID.pem') self.assertEqual( - sample_configs.sample_base_expected_config( + sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) @@ -121,16 +120,16 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( proto='TERMINATED_HTTPS', tls=True), - tls_cert=sample_configs.sample_tls_container_tuple( + tls_cert=sample_configs_split.sample_tls_container_tuple( id='tls_container_id', certificate='ImAalsdkfjCert', private_key='ImAsdlfksdjPrivateKey', primary_cn="FakeCN")) self.assertEqual( - sample_configs.sample_base_expected_config( + sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) @@ -154,10 +153,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple()) + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple()) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_member_backup(self): @@ -182,11 +181,11 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2 backup\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(monitor_ip_port=True, - backup_member=True)) + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + monitor_ip_port=True, backup_member=True)) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_custom_timeouts(self): @@ -219,13 +218,13 @@ class TestHaproxyCfg(base.TestCase): "sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(timeout_member_connect=1, - timeout_client_data=2, - timeout_member_data=3)) + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + timeout_member_connect=1, timeout_client_data=2, + timeout_member_data=3)) self.assertEqual( - sample_configs.sample_base_expected_config(frontend=fe, - backend=be), + sample_configs_split.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) def test_render_template_null_timeouts(self): @@ -258,13 +257,13 @@ class TestHaproxyCfg(base.TestCase): "sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(timeout_member_connect=None, - timeout_client_data=None, - timeout_member_data=None)) + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + timeout_member_connect=None, timeout_client_data=None, + timeout_member_data=None)) self.assertEqual( - sample_configs.sample_base_expected_config(frontend=fe, - backend=be), + sample_configs_split.sample_base_expected_config( + frontend=fe, backend=be), rendered_obj) def test_render_template_member_monitor_addr_port(self): @@ -289,10 +288,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(monitor_ip_port=True)) + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple(monitor_ip_port=True)) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_https_real_monitor(self): @@ -326,9 +325,9 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS')) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple(proto='HTTPS')) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_https_hello_monitor(self): @@ -361,10 +360,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( proto='HTTPS', monitor_proto='TLS-HELLO')) - self.assertEqual(sample_configs.sample_base_expected_config( + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_no_monitor_http(self): @@ -382,9 +381,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTP', monitor=False)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTP', monitor=False)) + self.assertEqual(sample_configs_split.sample_base_expected_config( backend=be), rendered_obj) def test_render_template_disabled_member(self): @@ -402,10 +402,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2 disabled\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTP', monitor=False, - disabled_member=True)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTP', monitor=False, disabled_member=True)) + self.assertEqual(sample_configs_split.sample_base_expected_config( backend=be), rendered_obj) def test_render_template_ping_monitor_http(self): @@ -430,10 +430,10 @@ class TestHaproxyCfg(base.TestCase): go = " maxconn {maxconn}\n external-check\n\n".format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTP', - monitor_proto='PING')) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTP', monitor_proto='PING')) + self.assertEqual(sample_configs_split.sample_base_expected_config( backend=be, global_opts=go), rendered_obj) def test_render_template_no_monitor_https(self): @@ -462,9 +462,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTPS', monitor=False)) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_health_monitor_http_check(self): @@ -488,11 +489,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTP', - monitor_proto='HTTP', - hm_host_http_check=True)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTP', monitor_proto='HTTP', hm_host_http_check=True)) + self.assertEqual(sample_configs_split.sample_base_expected_config( backend=be), rendered_obj) def test_render_template_no_persistence_https(self): @@ -518,10 +518,10 @@ class TestHaproxyCfg(base.TestCase): " server sample_member_id_2 10.0.0.98:82 " "weight 13\n\n").format(maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False, - persistence=False)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTPS', monitor=False, persistence=False)) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_no_persistence_http(self): @@ -536,10 +536,10 @@ class TestHaproxyCfg(base.TestCase): " server sample_member_id_2 10.0.0.98:82 " "weight 13\n\n").format(maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTP', monitor=False, - persistence=False)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTP', monitor=False, persistence=False)) + self.assertEqual(sample_configs_split.sample_base_expected_config( backend=be), rendered_obj) def test_render_template_sourceip_persistence(self): @@ -561,11 +561,11 @@ class TestHaproxyCfg(base.TestCase): "weight 13 check inter 30s fall 3 rise 2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( persistence_type='SOURCE_IP')) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_appcookie_persistence(self): @@ -588,12 +588,12 @@ class TestHaproxyCfg(base.TestCase): "weight 13 check inter 30s fall 3 rise 2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( persistence_type='APP_COOKIE', persistence_cookie='JSESSIONID')) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_unlimited_connections(self): @@ -622,9 +622,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTPS', monitor=False)) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_limited_connections(self): @@ -652,10 +653,10 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n") g_opts = " maxconn 2014\n\n" rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False, - connection_limit=2014)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( + proto='HTTPS', monitor=False, connection_limit=2014)) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be, global_opts=g_opts), rendered_obj) def test_render_template_l7policies(self): @@ -720,9 +721,9 @@ class TestHaproxyCfg(base.TestCase): "inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(l7=True)) - self.assertEqual(sample_configs.sample_base_expected_config( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple(l7=True)) + self.assertEqual(sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) def test_render_template_http_xff(self): @@ -746,11 +747,11 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( insert_headers={'X-Forwarded-For': 'true'})) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_http_xff_xfport(self): @@ -775,12 +776,12 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( insert_headers={'X-Forwarded-For': 'true', 'X-Forwarded-Port': 'true'})) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_pool_proxy_protocol(self): @@ -801,11 +802,11 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2 send-proxy\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( be_proto='PROXY')) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_pool_cert(self): @@ -831,15 +832,15 @@ class TestHaproxyCfg(base.TestCase): maxconn=constants.HAPROXY_MAX_MAXCONN, opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( pool_cert=True, tls_enabled=True), pool_tls_certs={ 'sample_pool_id_1': {'client_cert': cert_file_path, 'ca_cert': None, 'crl': None}}) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_render_template_with_full_pool_cert(self): @@ -870,8 +871,8 @@ class TestHaproxyCfg(base.TestCase): "crl-file %s" % pool_crl, "verify required sni ssl_fc_sni")) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple( + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple( pool_cert=True, pool_ca_cert=True, pool_crl=True, tls_enabled=True), pool_tls_certs={ @@ -880,107 +881,109 @@ class TestHaproxyCfg(base.TestCase): 'ca_cert': pool_ca_cert, 'crl': pool_crl}}) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_transform_session_persistence(self): - in_persistence = sample_configs.sample_session_persistence_tuple() - ret = self.jinja_cfg._transform_session_persistence(in_persistence, {}) - self.assertEqual(sample_configs.RET_PERSISTENCE, ret) + in_persistence = ( + sample_configs_split.sample_session_persistence_tuple()) + ret = self.jinja_cfg._transform_session_persistence( + in_persistence, {}) + self.assertEqual(sample_configs_split.RET_PERSISTENCE, ret) def test_transform_health_monitor(self): - in_persistence = sample_configs.sample_health_monitor_tuple() + in_persistence = sample_configs_split.sample_health_monitor_tuple() ret = self.jinja_cfg._transform_health_monitor(in_persistence, {}) - self.assertEqual(sample_configs.RET_MONITOR_1, ret) + self.assertEqual(sample_configs_split.RET_MONITOR_1, ret) def test_transform_member(self): - in_member = sample_configs.sample_member_tuple('sample_member_id_1', - '10.0.0.99') + in_member = sample_configs_split.sample_member_tuple( + 'sample_member_id_1', '10.0.0.99') ret = self.jinja_cfg._transform_member(in_member, {}) - self.assertEqual(sample_configs.RET_MEMBER_1, ret) + self.assertEqual(sample_configs_split.RET_MEMBER_1, ret) def test_transform_pool(self): - in_pool = sample_configs.sample_pool_tuple() + in_pool = sample_configs_split.sample_pool_tuple() ret = self.jinja_cfg._transform_pool(in_pool, {}) - self.assertEqual(sample_configs.RET_POOL_1, ret) + self.assertEqual(sample_configs_split.RET_POOL_1, ret) def test_transform_pool_2(self): - in_pool = sample_configs.sample_pool_tuple(sample_pool=2) + in_pool = sample_configs_split.sample_pool_tuple(sample_pool=2) ret = self.jinja_cfg._transform_pool(in_pool, {}) - self.assertEqual(sample_configs.RET_POOL_2, ret) + self.assertEqual(sample_configs_split.RET_POOL_2, ret) def test_transform_pool_http_reuse(self): - in_pool = sample_configs.sample_pool_tuple(sample_pool=2) + in_pool = sample_configs_split.sample_pool_tuple(sample_pool=2) ret = self.jinja_cfg._transform_pool( in_pool, {constants.HTTP_REUSE: True}) - expected_config = copy.copy(sample_configs.RET_POOL_2) + expected_config = copy.copy(sample_configs_split.RET_POOL_2) expected_config[constants.HTTP_REUSE] = True self.assertEqual(expected_config, ret) def test_transform_pool_cert(self): - in_pool = sample_configs.sample_pool_tuple(pool_cert=True) + in_pool = sample_configs_split.sample_pool_tuple(pool_cert=True) cert_path = os.path.join(self.jinja_cfg.base_crt_dir, 'test_listener_id', 'pool_cert.pem') ret = self.jinja_cfg._transform_pool( in_pool, {}, pool_tls_certs={'client_cert': cert_path}) - expected_config = copy.copy(sample_configs.RET_POOL_1) + expected_config = copy.copy(sample_configs_split.RET_POOL_1) expected_config['client_cert'] = cert_path self.assertEqual(expected_config, ret) def test_transform_listener(self): - in_listener = sample_configs.sample_listener_tuple() + in_listener = sample_configs_split.sample_listener_tuple() ret = self.jinja_cfg._transform_listener(in_listener, None, {}, in_listener.load_balancer) - self.assertEqual(sample_configs.RET_LISTENER, ret) + self.assertEqual(sample_configs_split.RET_LISTENER, ret) def test_transform_listener_with_l7(self): - in_listener = sample_configs.sample_listener_tuple(l7=True) + in_listener = sample_configs_split.sample_listener_tuple(l7=True) ret = self.jinja_cfg._transform_listener(in_listener, None, {}, in_listener.load_balancer) - self.assertEqual(sample_configs.RET_LISTENER_L7, ret) + self.assertEqual(sample_configs_split.RET_LISTENER_L7, ret) def test_transform_loadbalancer(self): - in_amphora = sample_configs.sample_amphora_tuple() - in_listener = sample_configs.sample_listener_tuple() + in_amphora = sample_configs_split.sample_amphora_tuple() + in_listener = sample_configs_split.sample_listener_tuple() ret = self.jinja_cfg._transform_loadbalancer( in_amphora, in_listener.load_balancer, in_listener, None, {}) - self.assertEqual(sample_configs.RET_LB, ret) + self.assertEqual(sample_configs_split.RET_LB, ret) def test_transform_amphora(self): - in_amphora = sample_configs.sample_amphora_tuple() + in_amphora = sample_configs_split.sample_amphora_tuple() ret = self.jinja_cfg._transform_amphora(in_amphora, {}) - self.assertEqual(sample_configs.RET_AMPHORA, ret) + self.assertEqual(sample_configs_split.RET_AMPHORA, ret) def test_transform_loadbalancer_with_l7(self): - in_amphora = sample_configs.sample_amphora_tuple() - in_listener = sample_configs.sample_listener_tuple(l7=True) + in_amphora = sample_configs_split.sample_amphora_tuple() + in_listener = sample_configs_split.sample_listener_tuple(l7=True) ret = self.jinja_cfg._transform_loadbalancer( in_amphora, in_listener.load_balancer, in_listener, None, {}) - self.assertEqual(sample_configs.RET_LB_L7, ret) + self.assertEqual(sample_configs_split.RET_LB_L7, ret) def test_transform_l7policy(self): - in_l7policy = sample_configs.sample_l7policy_tuple( + in_l7policy = sample_configs_split.sample_l7policy_tuple( 'sample_l7policy_id_1') ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) - self.assertEqual(sample_configs.RET_L7POLICY_1, ret) + self.assertEqual(sample_configs_split.RET_L7POLICY_1, ret) def test_transform_l7policy_2_8(self): - in_l7policy = sample_configs.sample_l7policy_tuple( + in_l7policy = sample_configs_split.sample_l7policy_tuple( 'sample_l7policy_id_2', sample_policy=2) ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) - self.assertEqual(sample_configs.RET_L7POLICY_2, ret) + self.assertEqual(sample_configs_split.RET_L7POLICY_2, ret) # test invalid action without redirect_http_code - in_l7policy = sample_configs.sample_l7policy_tuple( + in_l7policy = sample_configs_split.sample_l7policy_tuple( 'sample_l7policy_id_8', sample_policy=2, redirect_http_code=None) ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) - self.assertEqual(sample_configs.RET_L7POLICY_8, ret) + self.assertEqual(sample_configs_split.RET_L7POLICY_8, ret) def test_transform_l7policy_disabled_rule(self): - in_l7policy = sample_configs.sample_l7policy_tuple( + in_l7policy = sample_configs_split.sample_l7policy_tuple( 'sample_l7policy_id_6', sample_policy=6) ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}) - self.assertEqual(sample_configs.RET_L7POLICY_6, ret) + self.assertEqual(sample_configs_split.RET_L7POLICY_6, ret) def test_escape_haproxy_config_string(self): self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( @@ -1034,11 +1037,12 @@ class TestHaproxyCfg(base.TestCase): " option splice-response\n" " option http-keep-alive\n\n") rendered_obj = j_cfg.render_loadbalancer_obj( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple() + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple() ) self.assertEqual( - sample_configs.sample_base_expected_config(defaults=defaults), + sample_configs_split.sample_base_expected_config( + defaults=defaults), rendered_obj) def test_http_reuse(self): @@ -1065,12 +1069,12 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2 send-proxy\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = j_cfg.build_config( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(be_proto='PROXY'), + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple(be_proto='PROXY'), tls_cert=None, haproxy_versions=("1", "8", "1")) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) # Without http-reuse @@ -1091,12 +1095,12 @@ class TestHaproxyCfg(base.TestCase): "cookie sample_member_id_2 send-proxy\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN) rendered_obj = j_cfg.build_config( - sample_configs.sample_amphora_tuple(), - sample_configs.sample_listener_tuple(be_proto='PROXY'), + sample_configs_split.sample_amphora_tuple(), + sample_configs_split.sample_listener_tuple(be_proto='PROXY'), tls_cert=None, haproxy_versions=("1", "5", "18")) self.assertEqual( - sample_configs.sample_base_expected_config(backend=be), + sample_configs_split.sample_base_expected_config(backend=be), rendered_obj) def test_ssl_types_l7rules(self): @@ -1172,15 +1176,15 @@ class TestHaproxyCfg(base.TestCase): " timeout server 50000\n" " server sample_member_id_3 10.0.0.97:82 weight 13 check " "inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n") - sample_listener = sample_configs.sample_listener_tuple( + sample_listener = sample_configs_split.sample_listener_tuple( proto=constants.PROTOCOL_TERMINATED_HTTPS, l7=True, ssl_type_l7=True) rendered_obj = j_cfg.build_config( - sample_configs.sample_amphora_tuple(), + sample_configs_split.sample_amphora_tuple(), sample_listener, tls_cert=None, haproxy_versions=("1", "5", "18")) self.assertEqual( - sample_configs.sample_base_expected_config( + sample_configs_split.sample_base_expected_config( frontend=fe, backend=be), rendered_obj) diff --git a/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py b/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py index 05d6839982..6e1d8185c9 100644 --- a/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py @@ -16,7 +16,7 @@ from octavia.common import constants from octavia.common.jinja.lvs import jinja_cfg from octavia.tests.unit import base -from octavia.tests.unit.common.sample_configs import sample_configs +from octavia.tests.unit.common.sample_configs import sample_configs_combined from oslo_config import cfg from oslo_config import fixture as oslo_fixture @@ -76,7 +76,7 @@ class TestLvsCfg(base.TestCase): " }\n\n" "}\n\n") rendered_obj = self.udp_jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_listener_tuple( + sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, @@ -124,7 +124,7 @@ class TestLvsCfg(base.TestCase): " }\n\n" "}\n\n") - listener = sample_configs.sample_listener_tuple( + listener = sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT, connection_limit=98, @@ -172,7 +172,7 @@ class TestLvsCfg(base.TestCase): "}\n\n") rendered_obj = self.udp_jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_listener_tuple( + sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT, persistence=False, @@ -185,56 +185,57 @@ class TestLvsCfg(base.TestCase): "net_namespace amphora-haproxy\n\n\n") rendered_obj = self.udp_jinja_cfg.render_loadbalancer_obj( - sample_configs.sample_listener_tuple( + sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, monitor=False, persistence=False, alloc_default_pool=False)) self.assertEqual(exp, rendered_obj) def test_udp_transform_session_persistence(self): - persistence_src_ip = sample_configs.sample_session_persistence_tuple( - persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, - persistence_cookie=None, - persistence_timeout=33, - persistence_granularity='255.0.0.0' - ) - exp = sample_configs.UDP_SOURCE_IP_BODY + persistence_src_ip = ( + sample_configs_combined.sample_session_persistence_tuple( + persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, + persistence_cookie=None, + persistence_timeout=33, + persistence_granularity='255.0.0.0' + )) + exp = sample_configs_combined.UDP_SOURCE_IP_BODY ret = self.udp_jinja_cfg._transform_session_persistence( persistence_src_ip) self.assertEqual(exp, ret) def test_udp_transform_health_monitor(self): - in_hm = sample_configs.sample_health_monitor_tuple( + in_hm = sample_configs_combined.sample_health_monitor_tuple( proto=constants.HEALTH_MONITOR_UDP_CONNECT ) ret = self.udp_jinja_cfg._transform_health_monitor(in_hm) - self.assertEqual(sample_configs.RET_UDP_HEALTH_MONITOR, ret) + self.assertEqual(sample_configs_combined.RET_UDP_HEALTH_MONITOR, ret) def test_udp_transform_member(self): - in_member = sample_configs.sample_member_tuple('member_id_1', - '192.0.2.10') + in_member = sample_configs_combined.sample_member_tuple( + 'member_id_1', '192.0.2.10') ret = self.udp_jinja_cfg._transform_member(in_member) - self.assertEqual(sample_configs.RET_UDP_MEMBER, ret) + self.assertEqual(sample_configs_combined.RET_UDP_MEMBER, ret) def test_udp_transform_pool(self): - in_pool = sample_configs.sample_pool_tuple( + in_pool = sample_configs_combined.sample_pool_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, persistence_granularity='255.0.0.0', ) ret = self.udp_jinja_cfg._transform_pool(in_pool) - self.assertEqual(sample_configs.RET_UDP_POOL, ret) + self.assertEqual(sample_configs_combined.RET_UDP_POOL, ret) - in_pool = sample_configs.sample_pool_tuple( + in_pool = sample_configs_combined.sample_pool_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, persistence_granularity='255.0.0.0', monitor=False) - sample_configs.RET_UDP_POOL['health_monitor'] = '' + sample_configs_combined.RET_UDP_POOL['health_monitor'] = '' ret = self.udp_jinja_cfg._transform_pool(in_pool) - self.assertEqual(sample_configs.RET_UDP_POOL, ret) + self.assertEqual(sample_configs_combined.RET_UDP_POOL, ret) def test_udp_transform_listener(self): - in_listener = sample_configs.sample_listener_tuple( + in_listener = sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, @@ -243,9 +244,9 @@ class TestLvsCfg(base.TestCase): connection_limit=98 ) ret = self.udp_jinja_cfg._transform_listener(in_listener) - self.assertEqual(sample_configs.RET_UDP_LISTENER, ret) + self.assertEqual(sample_configs_combined.RET_UDP_LISTENER, ret) - in_listener = sample_configs.sample_listener_tuple( + in_listener = sample_configs_combined.sample_listener_tuple( proto=constants.PROTOCOL_UDP, persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, persistence_timeout=33, @@ -254,5 +255,5 @@ class TestLvsCfg(base.TestCase): connection_limit=-1) ret = self.udp_jinja_cfg._transform_listener(in_listener) - sample_configs.RET_UDP_LISTENER.pop('connection_limit') - self.assertEqual(sample_configs.RET_UDP_LISTENER, ret) + sample_configs_combined.RET_UDP_LISTENER.pop('connection_limit') + self.assertEqual(sample_configs_combined.RET_UDP_LISTENER, ret) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py new file mode 100644 index 0000000000..a4b5c442a1 --- /dev/null +++ b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py @@ -0,0 +1,1076 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import collections + +from oslo_config import cfg + +from octavia.common import constants +from octavia.tests.unit.common.sample_configs import sample_certs + +CONF = cfg.CONF + + +def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', + vrrp_ip='10.1.1.1', ha_ip='192.168.10.1', + vrrp_port_id='1234', ha_port_id='1234', role=None, + status='ACTIVE', vrrp_interface=None, + vrrp_priority=None, api_version='1.0'): + in_amphora = collections.namedtuple( + 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' + 'ha_port_id, role, status, vrrp_interface,' + 'vrrp_priority, api_version') + return in_amphora( + id=id, + lb_network_ip=lb_network_ip, + vrrp_ip=vrrp_ip, + ha_ip=ha_ip, + vrrp_port_id=vrrp_port_id, + ha_port_id=ha_port_id, + role=role, + status=status, + vrrp_interface=vrrp_interface, + vrrp_priority=vrrp_priority, + api_version=api_version) + + +RET_PERSISTENCE = { + 'type': 'HTTP_COOKIE', + 'cookie_name': None} + +RET_MONITOR_1 = { + 'id': 'sample_monitor_id_1', + 'type': 'HTTP', + 'delay': 30, + 'timeout': 31, + 'fall_threshold': 3, + 'rise_threshold': 2, + 'http_method': 'GET', + 'url_path': '/index.html', + 'expected_codes': '418', + 'enabled': True, + 'http_version': 1.0, + 'domain_name': None} + +RET_MONITOR_2 = { + 'id': 'sample_monitor_id_2', + 'type': 'HTTP', + 'delay': 30, + 'timeout': 31, + 'fall_threshold': 3, + 'rise_threshold': 2, + 'http_method': 'GET', + 'url_path': '/healthmon.html', + 'expected_codes': '418', + 'enabled': True, + 'http_version': 1.0, + 'domain_name': None} + +RET_MEMBER_1 = { + 'id': 'sample_member_id_1', + 'address': '10.0.0.99', + 'protocol_port': 82, + 'weight': 13, + 'subnet_id': '10.0.0.1/24', + 'enabled': True, + 'operating_status': 'ACTIVE', + 'monitor_address': None, + 'monitor_port': None, + 'backup': False} + +RET_MEMBER_2 = { + 'id': 'sample_member_id_2', + 'address': '10.0.0.98', + 'protocol_port': 82, + 'weight': 13, + 'subnet_id': '10.0.0.1/24', + 'enabled': True, + 'operating_status': 'ACTIVE', + 'monitor_address': None, + 'monitor_port': None, + 'backup': False} + +RET_MEMBER_3 = { + 'id': 'sample_member_id_3', + 'address': '10.0.0.97', + 'protocol_port': 82, + 'weight': 13, + 'subnet_id': '10.0.0.1/24', + 'enabled': True, + 'operating_status': 'ACTIVE', + 'monitor_address': None, + 'monitor_port': None, + 'backup': False} + +RET_POOL_1 = { + 'id': 'sample_pool_id_1', + 'protocol': 'http', + 'proxy_protocol': False, + 'lb_algorithm': 'roundrobin', + 'members': [RET_MEMBER_1, RET_MEMBER_2], + 'health_monitor': RET_MONITOR_1, + 'session_persistence': RET_PERSISTENCE, + 'enabled': True, + 'operating_status': 'ACTIVE', + 'stick_size': '10k', + constants.HTTP_REUSE: False, + 'ca_tls_path': '', + 'crl_path': '', + 'tls_enabled': False, +} + +RET_POOL_2 = { + 'id': 'sample_pool_id_2', + 'protocol': 'http', + 'proxy_protocol': False, + 'lb_algorithm': 'roundrobin', + 'members': [RET_MEMBER_3], + 'health_monitor': RET_MONITOR_2, + 'session_persistence': RET_PERSISTENCE, + 'enabled': True, + 'operating_status': 'ACTIVE', + 'stick_size': '10k', + constants.HTTP_REUSE: False, + 'ca_tls_path': '', + 'crl_path': '', + 'tls_enabled': False, +} + +RET_DEF_TLS_CONT = {'id': 'cont_id_1', 'allencompassingpem': 'imapem', + 'primary_cn': 'FakeCn'} +RET_SNI_CONT_1 = {'id': 'cont_id_2', 'allencompassingpem': 'imapem2', + 'primary_cn': 'FakeCn'} +RET_SNI_CONT_2 = {'id': 'cont_id_3', 'allencompassingpem': 'imapem3', + 'primary_cn': 'FakeCn2'} + +RET_L7RULE_1 = { + 'id': 'sample_l7rule_id_1', + 'type': constants.L7RULE_TYPE_PATH, + 'compare_type': constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + 'key': None, + 'value': '/api', + 'invert': False, + 'enabled': True} + +RET_L7RULE_2 = { + 'id': 'sample_l7rule_id_2', + 'type': constants.L7RULE_TYPE_HEADER, + 'compare_type': constants.L7RULE_COMPARE_TYPE_CONTAINS, + 'key': 'Some-header', + 'value': 'This\\ string\\\\\\ with\\ stuff', + 'invert': True, + 'enabled': True} + +RET_L7RULE_3 = { + 'id': 'sample_l7rule_id_3', + 'type': constants.L7RULE_TYPE_COOKIE, + 'compare_type': constants.L7RULE_COMPARE_TYPE_REGEX, + 'key': 'some-cookie', + 'value': 'this.*|that', + 'invert': False, + 'enabled': True} + +RET_L7RULE_4 = { + 'id': 'sample_l7rule_id_4', + 'type': constants.L7RULE_TYPE_FILE_TYPE, + 'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + 'key': None, + 'value': 'jpg', + 'invert': False, + 'enabled': True} + +RET_L7RULE_5 = { + 'id': 'sample_l7rule_id_5', + 'type': constants.L7RULE_TYPE_HOST_NAME, + 'compare_type': constants.L7RULE_COMPARE_TYPE_ENDS_WITH, + 'key': None, + 'value': '.example.com', + 'invert': False, + 'enabled': True} + +RET_L7RULE_6 = { + 'id': 'sample_l7rule_id_6', + 'type': constants.L7RULE_TYPE_HOST_NAME, + 'compare_type': constants.L7RULE_COMPARE_TYPE_ENDS_WITH, + 'key': None, + 'value': '.example.com', + 'invert': False, + 'enabled': False} + +RET_L7POLICY_1 = { + 'id': 'sample_l7policy_id_1', + 'action': constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + 'redirect_pool': RET_POOL_2, + 'redirect_url': None, + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [RET_L7RULE_1], + 'redirect_http_code': None} + +RET_L7POLICY_2 = { + 'id': 'sample_l7policy_id_2', + 'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL, + 'redirect_pool': None, + 'redirect_url': 'http://www.example.com', + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [RET_L7RULE_2, RET_L7RULE_3], + 'redirect_http_code': 302} + +RET_L7POLICY_3 = { + 'id': 'sample_l7policy_id_3', + 'action': constants.L7POLICY_ACTION_REJECT, + 'redirect_pool': None, + 'redirect_url': None, + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [RET_L7RULE_4, RET_L7RULE_5], + 'redirect_http_code': None} + +RET_L7POLICY_4 = { + 'id': 'sample_l7policy_id_4', + 'action': constants.L7POLICY_ACTION_REJECT, + 'redirect_pool': None, + 'redirect_url': None, + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [], + 'redirect_http_code': None} + +RET_L7POLICY_5 = { + 'id': 'sample_l7policy_id_5', + 'action': constants.L7POLICY_ACTION_REJECT, + 'redirect_pool': None, + 'redirect_url': None, + 'redirect_prefix': None, + 'enabled': False, + 'l7rules': [RET_L7RULE_5], + 'redirect_http_code': None} + +RET_L7POLICY_6 = { + 'id': 'sample_l7policy_id_6', + 'action': constants.L7POLICY_ACTION_REJECT, + 'redirect_pool': None, + 'redirect_url': None, + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [], + 'redirect_http_code': None} + +RET_L7POLICY_7 = { + 'id': 'sample_l7policy_id_7', + 'action': constants.L7POLICY_ACTION_REDIRECT_PREFIX, + 'redirect_pool': None, + 'redirect_url': None, + 'redirect_prefix': 'https://example.com', + 'enabled': True, + 'l7rules': [RET_L7RULE_2, RET_L7RULE_3], + 'redirect_http_code': 302} + +RET_L7POLICY_8 = { + 'id': 'sample_l7policy_id_8', + 'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL, + 'redirect_pool': None, + 'redirect_url': 'http://www.example.com', + 'redirect_prefix': None, + 'enabled': True, + 'l7rules': [RET_L7RULE_2, RET_L7RULE_3], + 'redirect_http_code': None} + +RET_LISTENER = { + 'id': 'sample_listener_id_1', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'protocol_mode': 'http', + 'default_pool': RET_POOL_1, + 'connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'user_log_format': '12345\\ sample_loadbalancer_id_1\\ %f\\ %ci\\ %cp\\ ' + '%t\\ %{+Q}r\\ %ST\\ %B\\ %U\\ %[ssl_c_verify]\\ ' + '%{+Q}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ %tsc', + 'pools': [RET_POOL_1], + 'l7policies': [], + 'enabled': True, + 'insert_headers': {}, + 'timeout_client_data': 50000, + 'timeout_member_connect': 5000, + 'timeout_member_data': 50000, + 'timeout_tcp_inspect': 0, +} + +RET_LISTENER_L7 = { + 'id': 'sample_listener_id_1', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'protocol_mode': 'http', + 'default_pool': RET_POOL_1, + 'connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'user_log_format': '12345\\ sample_loadbalancer_id_1\\ %f\\ %ci\\ %cp\\ ' + '%t\\ %{+Q}r\\ %ST\\ %B\\ %U\\ %[ssl_c_verify]\\ ' + '%{+Q}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ %tsc', + 'l7policies': [RET_L7POLICY_1, RET_L7POLICY_2, RET_L7POLICY_3, + RET_L7POLICY_4, RET_L7POLICY_5, RET_L7POLICY_6, + RET_L7POLICY_7], + 'pools': [RET_POOL_1, RET_POOL_2], + 'enabled': True, + 'insert_headers': {}, + 'timeout_client_data': 50000, + 'timeout_member_connect': 5000, + 'timeout_member_data': 50000, + 'timeout_tcp_inspect': 0, +} + +RET_LISTENER_TLS = { + 'id': 'sample_listener_id_1', + 'protocol_port': '443', + 'protocol': 'TERMINATED_HTTPS', + 'protocol_mode': 'http', + 'default_pool': RET_POOL_1, + 'connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'tls_certificate_id': 'cont_id_1', + 'default_tls_path': '/etc/ssl/sample_loadbalancer_id_1/fakeCN.pem', + 'default_tls_container': RET_DEF_TLS_CONT, + 'pools': [RET_POOL_1], + 'l7policies': [], + 'enabled': True, + 'insert_headers': {}} + +RET_LISTENER_TLS_SNI = { + 'id': 'sample_listener_id_1', + 'protocol_port': '443', + 'protocol': 'http', + 'protocol': 'TERMINATED_HTTPS', + 'default_pool': RET_POOL_1, + 'connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'tls_certificate_id': 'cont_id_1', + 'default_tls_path': '/etc/ssl/sample_loadbalancer_id_1/fakeCN.pem', + 'default_tls_container': RET_DEF_TLS_CONT, + 'crt_dir': '/v2/sample_loadbalancer_id_1', + 'sni_container_ids': ['cont_id_2', 'cont_id_3'], + 'sni_containers': [RET_SNI_CONT_1, RET_SNI_CONT_2], + 'pools': [RET_POOL_1], + 'l7policies': [], + 'enabled': True, + 'insert_headers': {}} + +RET_AMPHORA = { + 'id': 'sample_amphora_id_1', + 'lb_network_ip': '10.0.1.1', + 'vrrp_ip': '10.1.1.1', + 'ha_ip': '192.168.10.1', + 'vrrp_port_id': '1234', + 'ha_port_id': '1234', + 'role': None, + 'status': 'ACTIVE', + 'vrrp_interface': None, + 'vrrp_priority': None} + +RET_LB = { + 'host_amphora': RET_AMPHORA, + 'id': 'sample_loadbalancer_id_1', + 'vip_address': '10.0.0.2', + 'listeners': [RET_LISTENER], + 'peer_port': 1024, + 'topology': 'SINGLE', + 'enabled': True, + 'global_connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'amphorae': [sample_amphora_tuple()]} + +RET_LB_L7 = { + 'host_amphora': RET_AMPHORA, + 'id': 'sample_loadbalancer_id_1', + 'vip_address': '10.0.0.2', + 'listeners': [RET_LISTENER_L7], + 'peer_port': 1024, + 'topology': 'SINGLE', + 'enabled': True, + 'global_connection_limit': constants.HAPROXY_MAX_MAXCONN, + 'amphorae': [sample_amphora_tuple()]} + +UDP_SOURCE_IP_BODY = { + 'type': constants.SESSION_PERSISTENCE_SOURCE_IP, + 'persistence_timeout': 33, + 'persistence_granularity': '255.0.0.0' +} + +RET_UDP_HEALTH_MONITOR = { + 'id': 'sample_monitor_id_1', + 'type': constants.HEALTH_MONITOR_UDP_CONNECT, + 'delay': 30, + 'timeout': 31, + 'enabled': True, + 'fall_threshold': 3, + 'check_script_path': (CONF.haproxy_amphora.base_path + + '/lvs/check/udp_check.sh') +} + +UDP_HEALTH_MONITOR_NO_SCRIPT = { + 'id': 'sample_monitor_id_1', + 'check_script_path': None, + 'delay': 30, + 'enabled': True, + 'fall_threshold': 3, + 'timeout': 31, + 'type': 'UDP' +} + +RET_UDP_MEMBER = { + 'id': 'member_id_1', + 'address': '192.0.2.10', + 'protocol_port': 82, + 'weight': 13, + 'enabled': True +} + +UDP_MEMBER_1 = { + 'id': 'sample_member_id_1', + 'address': '10.0.0.99', + 'enabled': True, + 'protocol_port': 82, + 'weight': 13 +} + +UDP_MEMBER_2 = { + 'id': 'sample_member_id_2', + 'address': '10.0.0.98', + 'enabled': True, + 'protocol_port': 82, + 'weight': 13 +} + +RET_UDP_POOL = { + 'id': 'sample_pool_id_1', + 'enabled': True, + 'health_monitor': UDP_HEALTH_MONITOR_NO_SCRIPT, + 'lb_algorithm': 'rr', + 'members': [UDP_MEMBER_1, UDP_MEMBER_2], + 'protocol': 'udp', + 'session_persistence': UDP_SOURCE_IP_BODY +} + +RET_UDP_LISTENER = { + 'connection_limit': 98, + 'default_pool': { + 'id': 'sample_pool_id_1', + 'enabled': True, + 'health_monitor': RET_UDP_HEALTH_MONITOR, + 'lb_algorithm': 'rr', + 'members': [UDP_MEMBER_1, UDP_MEMBER_2], + 'protocol': 'udp', + 'session_persistence': UDP_SOURCE_IP_BODY + }, + 'enabled': True, + 'id': 'sample_listener_id_1', + 'protocol_mode': 'udp', + 'protocol_port': '80' +} + + +def sample_listener_loadbalancer_tuple( + proto=None, topology=None, enabled=True, pools=None): + proto = 'HTTP' if proto is None else proto + if topology and topology in ['ACTIVE_STANDBY', 'ACTIVE_ACTIVE']: + more_amp = True + else: + more_amp = False + topology = constants.TOPOLOGY_SINGLE + in_lb = collections.namedtuple( + 'load_balancer', 'id, name, protocol, vip, amphorae, topology, ' + 'pools, listeners, enabled, project_id') + return in_lb( + id='sample_loadbalancer_id_1', + name='test-lb', + protocol=proto, + vip=sample_vip_tuple(), + amphorae=[sample_amphora_tuple(role=constants.ROLE_MASTER), + sample_amphora_tuple( + id='sample_amphora_id_2', + lb_network_ip='10.0.1.2', + vrrp_ip='10.1.1.2', + role=constants.ROLE_BACKUP)] + if more_amp else [sample_amphora_tuple()], + topology=topology, + pools=pools or [], + listeners=[], + enabled=enabled, + project_id='12345', + ) + + +def sample_lb_with_udp_listener_tuple( + proto=None, topology=None, enabled=True, pools=None): + proto = 'HTTP' if proto is None else proto + if topology and topology in ['ACTIVE_STANDBY', 'ACTIVE_ACTIVE']: + more_amp = True + else: + more_amp = False + topology = constants.TOPOLOGY_SINGLE + listeners = [sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, + persistence_timeout=33, + persistence_granularity='255.255.0.0', + monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT)] + + in_lb = collections.namedtuple( + 'load_balancer', 'id, name, protocol, vip, amphorae, topology, ' + 'pools, enabled, project_id, listeners') + return in_lb( + id='sample_loadbalancer_id_1', + name='test-lb', + protocol=proto, + vip=sample_vip_tuple(), + amphorae=[sample_amphora_tuple(role=constants.ROLE_MASTER), + sample_amphora_tuple( + id='sample_amphora_id_2', + lb_network_ip='10.0.1.2', + vrrp_ip='10.1.1.2', + role=constants.ROLE_BACKUP)] + if more_amp else [sample_amphora_tuple()], + topology=topology, + listeners=listeners, + pools=pools or [], + enabled=enabled, + project_id='12345' + ) + + +def sample_vrrp_group_tuple(): + in_vrrp_group = collections.namedtuple( + 'vrrp_group', 'load_balancer_id, vrrp_auth_type, vrrp_auth_pass, ' + 'advert_int, smtp_server, smtp_connect_timeout, ' + 'vrrp_group_name') + return in_vrrp_group( + vrrp_group_name='sample_loadbalancer_id_1', + load_balancer_id='sample_loadbalancer_id_1', + vrrp_auth_type='PASS', + vrrp_auth_pass='123', + advert_int='1', + smtp_server='', + smtp_connect_timeout='') + + +def sample_vip_tuple(): + vip = collections.namedtuple('vip', 'ip_address') + return vip(ip_address='10.0.0.2') + + +def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, + persistence=True, persistence_type=None, + persistence_cookie=None, persistence_timeout=None, + persistence_granularity=None, + tls=False, sni=False, peer_port=None, topology=None, + l7=False, enabled=True, insert_headers=None, + be_proto=None, monitor_ip_port=False, + monitor_proto=None, backup_member=False, + disabled_member=False, connection_limit=-1, + timeout_client_data=50000, + timeout_member_connect=5000, + timeout_member_data=50000, + timeout_tcp_inspect=0, + client_ca_cert=False, client_crl_cert=False, + ssl_type_l7=False, pool_cert=False, + pool_ca_cert=False, pool_crl=False, + tls_enabled=False, hm_host_http_check=False, + id='sample_listener_id_1', recursive_nest=False): + proto = 'HTTP' if proto is None else proto + if be_proto is None: + be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto + topology = 'SINGLE' if topology is None else topology + port = '443' if proto is 'HTTPS' or proto is 'TERMINATED_HTTPS' else '80' + peer_port = 1024 if peer_port is None else peer_port + insert_headers = insert_headers or {} + in_listener = collections.namedtuple( + 'listener', 'id, project_id, protocol_port, protocol, default_pool, ' + 'connection_limit, tls_certificate_id, ' + 'sni_container_ids, default_tls_container, ' + 'sni_containers, load_balancer, peer_port, pools, ' + 'l7policies, enabled, insert_headers, timeout_client_data,' + 'timeout_member_connect, timeout_member_data, ' + 'timeout_tcp_inspect, client_ca_tls_certificate_id, ' + 'client_ca_tls_certificate, client_authentication, ' + 'client_crl_container_id') + if l7: + pools = [ + sample_pool_tuple( + proto=be_proto, monitor=monitor, persistence=persistence, + persistence_type=persistence_type, + persistence_cookie=persistence_cookie, + monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + pool_cert=pool_cert, pool_ca_cert=pool_ca_cert, + pool_crl=pool_crl, tls_enabled=tls_enabled, + hm_host_http_check=hm_host_http_check, + listener_id='sample_listener_id_1'), + sample_pool_tuple( + proto=be_proto, monitor=monitor, persistence=persistence, + persistence_type=persistence_type, + persistence_cookie=persistence_cookie, sample_pool=2, + monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + pool_cert=pool_cert, pool_ca_cert=pool_ca_cert, + pool_crl=pool_crl, tls_enabled=tls_enabled, + hm_host_http_check=hm_host_http_check, + listener_id='sample_listener_id_1')] + l7policies = [ + sample_l7policy_tuple('sample_l7policy_id_1', sample_policy=1), + sample_l7policy_tuple('sample_l7policy_id_2', sample_policy=2), + sample_l7policy_tuple('sample_l7policy_id_3', sample_policy=3), + sample_l7policy_tuple('sample_l7policy_id_4', sample_policy=4), + sample_l7policy_tuple('sample_l7policy_id_5', sample_policy=5), + sample_l7policy_tuple('sample_l7policy_id_6', sample_policy=6), + sample_l7policy_tuple('sample_l7policy_id_7', sample_policy=7)] + if ssl_type_l7: + l7policies.append(sample_l7policy_tuple( + 'sample_l7policy_id_8', sample_policy=8)) + else: + pools = [ + sample_pool_tuple( + proto=be_proto, monitor=monitor, persistence=persistence, + persistence_type=persistence_type, + persistence_cookie=persistence_cookie, + monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + backup_member=backup_member, disabled_member=disabled_member, + pool_cert=pool_cert, pool_ca_cert=pool_ca_cert, + pool_crl=pool_crl, tls_enabled=tls_enabled, + hm_host_http_check=hm_host_http_check, + listener_id='sample_listener_id_1')] + l7policies = [] + listener = in_listener( + id=id, + project_id='12345', + protocol_port=port, + protocol=proto, + load_balancer=sample_listener_loadbalancer_tuple( + proto=proto, topology=topology, pools=pools), + peer_port=peer_port, + default_pool=sample_pool_tuple( + listener_id='sample_listener_id_1', + proto=be_proto, monitor=monitor, persistence=persistence, + persistence_type=persistence_type, + persistence_cookie=persistence_cookie, + persistence_timeout=persistence_timeout, + persistence_granularity=persistence_granularity, + monitor_ip_port=monitor_ip_port, + monitor_proto=monitor_proto, + pool_cert=pool_cert, + pool_ca_cert=pool_ca_cert, + pool_crl=pool_crl, + tls_enabled=tls_enabled, + hm_host_http_check=hm_host_http_check + ) if alloc_default_pool else '', + connection_limit=connection_limit, + tls_certificate_id='cont_id_1' if tls else '', + sni_container_ids=['cont_id_2', 'cont_id_3'] if sni else [], + default_tls_container=sample_tls_container_tuple( + id='cont_id_1', certificate=sample_certs.X509_CERT, + private_key=sample_certs.X509_CERT_KEY, + intermediates=sample_certs.X509_IMDS_LIST, + primary_cn=sample_certs.X509_CERT_CN + ) if tls else '', + sni_containers=[ + sample_tls_sni_container_tuple( + tls_container_id='cont_id_2', + tls_container=sample_tls_container_tuple( + id='cont_id_2', certificate=sample_certs.X509_CERT_2, + private_key=sample_certs.X509_CERT_KEY_2, + intermediates=sample_certs.X509_IMDS_LIST, + primary_cn=sample_certs.X509_CERT_CN_2)), + sample_tls_sni_container_tuple( + tls_container_id='cont_id_3', + tls_container=sample_tls_container_tuple( + id='cont_id_3', certificate=sample_certs.X509_CERT_3, + private_key=sample_certs.X509_CERT_KEY_3, + intermediates=sample_certs.X509_IMDS_LIST, + primary_cn=sample_certs.X509_CERT_CN_3))] + if sni else [], + pools=pools, + l7policies=l7policies, + enabled=enabled, + insert_headers=insert_headers, + timeout_client_data=timeout_client_data, + timeout_member_connect=timeout_member_connect, + timeout_member_data=timeout_member_data, + timeout_tcp_inspect=timeout_tcp_inspect, + client_ca_tls_certificate_id='cont_id_ca' if client_ca_cert else '', + client_ca_tls_certificate=sample_tls_container_tuple( + id='cont_id_ca', certificate=sample_certs.X509_CA_CERT, + primary_cn=sample_certs.X509_CA_CERT_CN + ) if client_ca_cert else '', + client_authentication=( + constants.CLIENT_AUTH_MANDATORY if client_ca_cert else + constants.CLIENT_AUTH_NONE), + client_crl_container_id='cont_id_crl' if client_crl_cert else '', + ) + if recursive_nest: + listener.load_balancer.listeners.append(listener) + return listener + + +def sample_tls_sni_container_tuple(tls_container_id=None, tls_container=None): + sc = collections.namedtuple('sni_container', 'tls_container_id, ' + 'tls_container') + return sc(tls_container_id=tls_container_id, tls_container=tls_container) + + +def sample_tls_sni_containers_tuple(tls_container_id=None, tls_container=None): + sc = collections.namedtuple('sni_containers', 'tls_container_id, ' + 'tls_container') + return [sc(tls_container_id=tls_container_id, tls_container=tls_container)] + + +def sample_tls_container_tuple(id='cont_id_1', certificate=None, + private_key=None, intermediates=None, + primary_cn=None): + sc = collections.namedtuple( + 'tls_container', + 'id, certificate, private_key, intermediates, primary_cn') + return sc(id=id, certificate=certificate, private_key=private_key, + intermediates=intermediates or [], primary_cn=primary_cn) + + +def sample_pool_tuple(listener_id=None, proto=None, monitor=True, + persistence=True, persistence_type=None, + persistence_cookie=None, persistence_timeout=None, + persistence_granularity=None, sample_pool=1, + monitor_ip_port=False, monitor_proto=None, + backup_member=False, disabled_member=False, + has_http_reuse=True, pool_cert=False, pool_ca_cert=False, + pool_crl=False, tls_enabled=False, + hm_host_http_check=False): + proto = 'HTTP' if proto is None else proto + monitor_proto = proto if monitor_proto is None else monitor_proto + in_pool = collections.namedtuple( + 'pool', 'id, protocol, lb_algorithm, members, health_monitor, ' + 'session_persistence, enabled, operating_status, ' + 'tls_certificate_id, ca_tls_certificate_id, ' + 'crl_container_id, tls_enabled, ' + constants.HTTP_REUSE) + if (proto == constants.PROTOCOL_UDP and + persistence_type == constants.SESSION_PERSISTENCE_SOURCE_IP): + kwargs = {'persistence_type': persistence_type, + 'persistence_timeout': persistence_timeout, + 'persistence_granularity': persistence_granularity} + else: + kwargs = {'persistence_type': persistence_type, + 'persistence_cookie': persistence_cookie} + persis = sample_session_persistence_tuple(**kwargs) + mon = None + if sample_pool == 1: + id = 'sample_pool_id_1' + members = [sample_member_tuple('sample_member_id_1', '10.0.0.99', + monitor_ip_port=monitor_ip_port), + sample_member_tuple('sample_member_id_2', '10.0.0.98', + monitor_ip_port=monitor_ip_port, + backup=backup_member, + enabled=not disabled_member)] + if monitor is True: + mon = sample_health_monitor_tuple( + proto=monitor_proto, host_http_check=hm_host_http_check) + elif sample_pool == 2: + id = 'sample_pool_id_2' + members = [sample_member_tuple('sample_member_id_3', '10.0.0.97', + monitor_ip_port=monitor_ip_port)] + if monitor is True: + mon = sample_health_monitor_tuple( + proto=monitor_proto, sample_hm=2, + host_http_check=hm_host_http_check) + return in_pool( + id=id, + protocol=proto, + lb_algorithm='ROUND_ROBIN', + members=members, + health_monitor=mon, + session_persistence=persis if persistence is True else None, + enabled=True, + operating_status='ACTIVE', has_http_reuse=has_http_reuse, + tls_certificate_id='pool_cont_1' if pool_cert else None, + ca_tls_certificate_id='pool_ca_1' if pool_ca_cert else None, + crl_container_id='pool_crl' if pool_crl else None, + tls_enabled=tls_enabled) + + +def sample_member_tuple(id, ip, enabled=True, operating_status='ACTIVE', + monitor_ip_port=False, backup=False): + in_member = collections.namedtuple('member', + 'id, ip_address, protocol_port, ' + 'weight, subnet_id, ' + 'enabled, operating_status, ' + 'monitor_address, monitor_port, ' + 'backup') + monitor_address = '192.168.1.1' if monitor_ip_port else None + monitor_port = 9000 if monitor_ip_port else None + return in_member( + id=id, + ip_address=ip, + protocol_port=82, + weight=13, + subnet_id='10.0.0.1/24', + enabled=enabled, + operating_status=operating_status, + monitor_address=monitor_address, + monitor_port=monitor_port, + backup=backup) + + +def sample_session_persistence_tuple(persistence_type=None, + persistence_cookie=None, + persistence_timeout=None, + persistence_granularity=None): + spersistence = collections.namedtuple('SessionPersistence', + 'type, cookie_name, ' + 'persistence_timeout, ' + 'persistence_granularity') + pt = 'HTTP_COOKIE' if persistence_type is None else persistence_type + return spersistence(type=pt, + cookie_name=persistence_cookie, + persistence_timeout=persistence_timeout, + persistence_granularity=persistence_granularity) + + +def sample_health_monitor_tuple(proto='HTTP', sample_hm=1, + host_http_check=False): + proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto + monitor = collections.namedtuple( + 'monitor', 'id, type, delay, timeout, fall_threshold, rise_threshold,' + 'http_method, url_path, expected_codes, enabled, ' + 'check_script_path, http_version, domain_name') + + if sample_hm == 1: + id = 'sample_monitor_id_1' + url_path = '/index.html' + elif sample_hm == 2: + id = 'sample_monitor_id_2' + url_path = '/healthmon.html' + kwargs = { + 'id': id, + 'type': proto, + 'delay': 30, + 'timeout': 31, + 'fall_threshold': 3, + 'rise_threshold': 2, + 'http_method': 'GET', + 'url_path': url_path, + 'expected_codes': '418', + 'enabled': True + } + if host_http_check: + kwargs.update({'http_version': 1.1, 'domain_name': 'testlab.com'}) + else: + kwargs.update({'http_version': 1.0, 'domain_name': None}) + if proto == constants.HEALTH_MONITOR_UDP_CONNECT: + kwargs['check_script_path'] = (CONF.haproxy_amphora.base_path + + 'lvs/check/' + 'udp_check.sh') + else: + kwargs['check_script_path'] = None + return monitor(**kwargs) + + +def sample_l7policy_tuple(id, + action=constants.L7POLICY_ACTION_REJECT, + redirect_pool=None, redirect_url=None, + redirect_prefix=None, + enabled=True, redirect_http_code=302, + sample_policy=1): + in_l7policy = collections.namedtuple('l7policy', + 'id, action, redirect_pool, ' + 'redirect_url, redirect_prefix, ' + 'l7rules, enabled,' + 'redirect_http_code') + l7rules = [] + if sample_policy == 1: + action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL + redirect_pool = sample_pool_tuple(sample_pool=2) + l7rules = [sample_l7rule_tuple('sample_l7rule_id_1')] + elif sample_policy == 2: + action = constants.L7POLICY_ACTION_REDIRECT_TO_URL + redirect_url = 'http://www.example.com' + l7rules = [sample_l7rule_tuple('sample_l7rule_id_2', sample_rule=2), + sample_l7rule_tuple('sample_l7rule_id_3', sample_rule=3)] + elif sample_policy == 3: + action = constants.L7POLICY_ACTION_REJECT + l7rules = [sample_l7rule_tuple('sample_l7rule_id_4', sample_rule=4), + sample_l7rule_tuple('sample_l7rule_id_5', sample_rule=5)] + elif sample_policy == 4: + action = constants.L7POLICY_ACTION_REJECT + elif sample_policy == 5: + action = constants.L7POLICY_ACTION_REJECT + enabled = False + l7rules = [sample_l7rule_tuple('sample_l7rule_id_5', sample_rule=5)] + elif sample_policy == 6: + action = constants.L7POLICY_ACTION_REJECT + l7rules = [sample_l7rule_tuple('sample_l7rule_id_6', sample_rule=6)] + elif sample_policy == 7: + action = constants.L7POLICY_ACTION_REDIRECT_PREFIX + redirect_prefix = 'https://example.com' + l7rules = [sample_l7rule_tuple('sample_l7rule_id_2', sample_rule=2), + sample_l7rule_tuple('sample_l7rule_id_3', sample_rule=3)] + elif sample_policy == 8: + action = constants.L7POLICY_ACTION_REDIRECT_TO_URL + redirect_url = 'http://www.ssl-type-l7rule-test.com' + l7rules = [sample_l7rule_tuple('sample_l7rule_id_7', sample_rule=7), + sample_l7rule_tuple('sample_l7rule_id_8', sample_rule=8), + sample_l7rule_tuple('sample_l7rule_id_9', sample_rule=9), + sample_l7rule_tuple('sample_l7rule_id_10', sample_rule=10), + sample_l7rule_tuple('sample_l7rule_id_11', sample_rule=11)] + return in_l7policy( + id=id, + action=action, + redirect_pool=redirect_pool, + redirect_url=redirect_url, + redirect_prefix=redirect_prefix, + l7rules=l7rules, + enabled=enabled, + redirect_http_code=redirect_http_code + if (action in [constants.L7POLICY_ACTION_REDIRECT_TO_URL, + constants.L7POLICY_ACTION_REDIRECT_PREFIX] and + redirect_http_code) else None) + + +def sample_l7rule_tuple(id, + type=constants.L7RULE_TYPE_PATH, + compare_type=constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + key=None, + value='/api', + invert=False, + enabled=True, + sample_rule=1): + in_l7rule = collections.namedtuple('l7rule', + 'id, type, compare_type, ' + 'key, value, invert, enabled') + if sample_rule == 2: + type = constants.L7RULE_TYPE_HEADER + compare_type = constants.L7RULE_COMPARE_TYPE_CONTAINS + key = 'Some-header' + value = 'This string\\ with stuff' + invert = True + enabled = True + if sample_rule == 3: + type = constants.L7RULE_TYPE_COOKIE + compare_type = constants.L7RULE_COMPARE_TYPE_REGEX + key = 'some-cookie' + value = 'this.*|that' + invert = False + enabled = True + if sample_rule == 4: + type = constants.L7RULE_TYPE_FILE_TYPE + compare_type = constants.L7RULE_COMPARE_TYPE_EQUAL_TO + key = None + value = 'jpg' + invert = False + enabled = True + if sample_rule == 5: + type = constants.L7RULE_TYPE_HOST_NAME + compare_type = constants.L7RULE_COMPARE_TYPE_ENDS_WITH + key = None + value = '.example.com' + invert = False + enabled = True + if sample_rule == 6: + type = constants.L7RULE_TYPE_HOST_NAME + compare_type = constants.L7RULE_COMPARE_TYPE_ENDS_WITH + key = None + value = '.example.com' + invert = False + enabled = False + if sample_rule == 7: + type = constants.L7RULE_TYPE_SSL_CONN_HAS_CERT + compare_type = constants.L7RULE_COMPARE_TYPE_EQUAL_TO + key = None + value = 'tRuE' + invert = False + enabled = True + if sample_rule == 8: + type = constants.L7RULE_TYPE_SSL_VERIFY_RESULT + compare_type = constants.L7RULE_COMPARE_TYPE_EQUAL_TO + key = None + value = '1' + invert = True + enabled = True + if sample_rule == 9: + type = constants.L7RULE_TYPE_SSL_DN_FIELD + compare_type = constants.L7RULE_COMPARE_TYPE_REGEX + key = 'STREET' + value = r'^STREET.*NO\.$' + invert = True + enabled = True + if sample_rule == 10: + type = constants.L7RULE_TYPE_SSL_DN_FIELD + compare_type = constants.L7RULE_COMPARE_TYPE_STARTS_WITH + key = 'OU-3' + value = 'Orgnization Bala' + invert = True + enabled = True + return in_l7rule( + id=id, + type=type, + compare_type=compare_type, + key=key, + value=value, + invert=invert, + enabled=enabled) + + +def sample_base_expected_config(frontend=None, backend=None, + peers=None, global_opts=None, defaults=None): + if frontend is None: + frontend = ("frontend sample_listener_id_1\n" + " log-format 12345\\ sample_loadbalancer_id_1\\ %f\\ " + "%ci\\ %cp\\ %t\\ %{{+Q}}r\\ %ST\\ %B\\ %U\\ " + "%[ssl_c_verify]\\ %{{+Q}}[ssl_c_s_dn]\\ %b\\ %s\\ %Tt\\ " + "%tsc\n" + " maxconn {maxconn}\n" + " bind 10.0.0.2:80\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1" + "\n" + " timeout client 50000\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + if backend is None: + backend = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2\n" + "\n").format(maxconn=constants.HAPROXY_MAX_MAXCONN) + + if peers is None: + peers = "\n\n" + if global_opts is None: + global_opts = " maxconn {maxconn}\n\n".format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + if defaults is None: + defaults = ("defaults\n" + " log global\n" + " retries 3\n" + " option redispatch\n" + " option splice-request\n" + " option splice-response\n" + " option http-keep-alive\n\n") + return ("# Configuration for loadbalancer sample_loadbalancer_id_1\n" + "global\n" + " daemon\n" + " user nobody\n" + " log /dev/log local0\n" + " log /dev/log local1 notice\n" + " stats socket /var/lib/octavia/sample_loadbalancer_id_1.sock" + " mode 0666 level user\n" + + global_opts + defaults + peers + frontend + backend) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs.py b/octavia/tests/unit/common/sample_configs/sample_configs_split.py similarity index 95% rename from octavia/tests/unit/common/sample_configs/sample_configs.py rename to octavia/tests/unit/common/sample_configs/sample_configs_split.py index b0ad9bed74..03374fd365 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_split.py @@ -27,11 +27,11 @@ def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', vrrp_ip='10.1.1.1', ha_ip='192.168.10.1', vrrp_port_id='1234', ha_port_id='1234', role=None, status='ACTIVE', vrrp_interface=None, - vrrp_priority=None): + vrrp_priority=None, api_version='0.5'): in_amphora = collections.namedtuple( 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' 'ha_port_id, role, status, vrrp_interface,' - 'vrrp_priority') + 'vrrp_priority, api_version') return in_amphora( id=id, lb_network_ip=lb_network_ip, @@ -42,7 +42,8 @@ def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', role=role, status=status, vrrp_interface=vrrp_interface, - vrrp_priority=vrrp_priority) + vrrp_priority=vrrp_priority, + api_version=api_version) RET_PERSISTENCE = { @@ -510,7 +511,7 @@ def sample_listener_loadbalancer_tuple(proto=None, topology=None, topology = constants.TOPOLOGY_SINGLE in_lb = collections.namedtuple( 'load_balancer', 'id, name, protocol, vip, amphorae, topology, ' - 'enabled, project_id') + 'listeners, enabled, project_id') return in_lb( id='sample_loadbalancer_id_1', name='test-lb', @@ -524,6 +525,45 @@ def sample_listener_loadbalancer_tuple(proto=None, topology=None, role=constants.ROLE_BACKUP)] if more_amp else [sample_amphora_tuple()], topology=topology, + listeners=[], + enabled=enabled, + project_id='12345' + ) + + +def sample_lb_with_udp_listener_tuple( + proto=None, topology=None, enabled=True, pools=None): + proto = 'HTTP' if proto is None else proto + if topology and topology in ['ACTIVE_STANDBY', 'ACTIVE_ACTIVE']: + more_amp = True + else: + more_amp = False + topology = constants.TOPOLOGY_SINGLE + listeners = [sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, + persistence_timeout=33, + persistence_granularity='255.255.0.0', + monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT)] + + in_lb = collections.namedtuple( + 'load_balancer', 'id, name, protocol, vip, amphorae, topology, ' + 'pools, enabled, project_id, listeners') + return in_lb( + id='sample_loadbalancer_id_1', + name='test-lb', + protocol=proto, + vip=sample_vip_tuple(), + amphorae=[sample_amphora_tuple(role=constants.ROLE_MASTER), + sample_amphora_tuple( + id='sample_amphora_id_2', + lb_network_ip='10.0.1.2', + vrrp_ip='10.1.1.2', + role=constants.ROLE_BACKUP)] + if more_amp else [sample_amphora_tuple()], + topology=topology, + listeners=listeners, + pools=pools or [], enabled=enabled, project_id='12345' ) @@ -565,7 +605,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, client_ca_cert=False, client_crl_cert=False, ssl_type_l7=False, pool_cert=False, pool_ca_cert=False, pool_crl=False, - tls_enabled=False, hm_host_http_check=False): + tls_enabled=False, hm_host_http_check=False, + id='sample_listener_id_1', recursive_nest=False): proto = 'HTTP' if proto is None else proto if be_proto is None: be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto @@ -624,8 +665,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, pool_crl=pool_crl, tls_enabled=tls_enabled, hm_host_http_check=hm_host_http_check)] l7policies = [] - return in_listener( - id='sample_listener_id_1', + listener = in_listener( + id=id, project_id='12345', protocol_port=port, protocol=proto, @@ -689,6 +730,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, constants.CLIENT_AUTH_NONE), client_crl_container_id='cont_id_crl' if client_crl_cert else '', ) + if recursive_nest: + listener.load_balancer.listeners.append(listener) + return listener def sample_tls_sni_container_tuple(tls_container_id=None, tls_container=None): diff --git a/octavia/tests/unit/common/tls_utils/test_cert_parser.py b/octavia/tests/unit/common/tls_utils/test_cert_parser.py index 6870e92ca7..578f453188 100644 --- a/octavia/tests/unit/common/tls_utils/test_cert_parser.py +++ b/octavia/tests/unit/common/tls_utils/test_cert_parser.py @@ -22,7 +22,7 @@ import octavia.common.exceptions as exceptions import octavia.common.tls_utils.cert_parser as cert_parser from octavia.tests.unit import base from octavia.tests.unit.common.sample_configs import sample_certs -from octavia.tests.unit.common.sample_configs import sample_configs +from octavia.tests.unit.common.sample_configs import sample_configs_combined class TestTLSParseUtils(base.TestCase): @@ -144,8 +144,8 @@ class TestTLSParseUtils(base.TestCase): @mock.patch('oslo_context.context.RequestContext') def test_load_certificates(self, mock_oslo): - listener = sample_configs.sample_listener_tuple(tls=True, sni=True, - client_ca_cert=True) + listener = sample_configs_combined.sample_listener_tuple( + tls=True, sni=True, client_ca_cert=True) client = mock.MagicMock() context = mock.Mock() context.project_id = '12345' @@ -165,8 +165,8 @@ class TestTLSParseUtils(base.TestCase): client.assert_has_calls(calls_cert_mngr) # Test asking for nothing - listener = sample_configs.sample_listener_tuple(tls=False, sni=False, - client_ca_cert=False) + listener = sample_configs_combined.sample_listener_tuple( + tls=False, sni=False, client_ca_cert=False) client = mock.MagicMock() with mock.patch.object(cert_parser, '_map_cert_tls_container') as mock_map: @@ -211,7 +211,7 @@ class TestTLSParseUtils(base.TestCase): def test_build_pem(self): expected = b'imacert\nimakey\nimainter\nimainter2\n' - tls_tuple = sample_configs.sample_tls_container_tuple( + tls_tuple = sample_configs_combined.sample_tls_container_tuple( certificate=b'imacert', private_key=b'imakey', intermediates=[b'imainter', b'imainter2']) self.assertEqual(expected, cert_parser.build_pem(tls_tuple)) diff --git a/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py b/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py index ffc9bfc1e9..b0fd0078fd 100644 --- a/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py +++ b/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py @@ -136,7 +136,8 @@ class TestUpdateHealthDb(base.TestCase): if listener: listener_ref = {'listener-id-1': { constants.OPERATING_STATUS: 'bogus', - 'protocol': listener_protocol}} + 'protocol': listener_protocol, + 'enabled': True}} lb_ref['listeners'] = listener_ref return lb_ref @@ -145,6 +146,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time() } @@ -161,6 +163,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time() } @@ -178,6 +181,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time() } @@ -194,6 +198,7 @@ class TestUpdateHealthDb(base.TestCase): hb_interval = cfg.CONF.health_manager.heartbeat_interval health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time() - hb_interval - 1 # extra -1 for buffer } @@ -211,6 +216,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -233,9 +239,9 @@ class TestUpdateHealthDb(base.TestCase): self.session_mock.rollback.assert_called_once() def test_update_health_online(self): - health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -281,10 +287,53 @@ class TestUpdateHealthDb(base.TestCase): self.hm.update_health(health, '192.0.2.1') self.assertTrue(not self.amphora_health_repo.replace.called) + def test_update_health_listener_disabled(self): + health = { + "id": self.FAKE_UUID_1, + "ver": 1, + "listeners": { + "listener-id-1": {"status": constants.OPEN, "pools": { + "pool-id-1": {"status": constants.UP, + "members": {"member-id-1": constants.UP} + } + } + } + }, + "recv_time": time.time() + } + + lb_ref = self._make_fake_lb_health_dict() + lb_ref['listeners']['listener-id-2'] = { + 'enabled': False, constants.OPERATING_STATUS: constants.OFFLINE} + self.amphora_repo.get_lb_for_health_update.return_value = lb_ref + self.hm.update_health(health, '192.0.2.1') + self.assertTrue(self.amphora_health_repo.replace.called) + + # test listener, member + for listener_id, listener in six.iteritems( + health.get('listeners', {})): + + self.listener_repo.update.assert_any_call( + self.session_mock, listener_id, + operating_status=constants.ONLINE) + + for pool_id, pool in six.iteritems(listener.get('pools', {})): + + self.hm.pool_repo.update.assert_any_call( + self.session_mock, pool_id, + operating_status=constants.ONLINE) + + for member_id, member in six.iteritems( + pool.get('members', {})): + self.member_repo.update.assert_any_call( + self.session_mock, member_id, + operating_status=constants.ONLINE) + def test_update_lb_pool_health_offline(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": {}} }, @@ -312,6 +361,7 @@ class TestUpdateHealthDb(base.TestCase): def test_update_lb_multiple_listeners_one_error_pool(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.DOWN, @@ -333,7 +383,8 @@ class TestUpdateHealthDb(base.TestCase): lb_ref['listeners']['listener-id-2'] = { constants.OPERATING_STATUS: 'bogus', - 'protocol': constants.PROTOCOL_TCP} + 'protocol': constants.PROTOCOL_TCP, + 'enabled': True} self.amphora_repo.get_lb_for_health_update.return_value = lb_ref self.hm.update_health(health, '192.0.2.1') @@ -359,6 +410,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -396,10 +448,54 @@ class TestUpdateHealthDb(base.TestCase): self.session_mock, member_id, operating_status=constants.ONLINE) + def test_update_v2_lb_and_list_pool_health_online(self): + + health = { + "id": self.FAKE_UUID_1, + "ver": 2, + "listeners": { + "listener-id-1": {"status": constants.OPEN} + }, + "pools": { + "pool-id-1:listener-id-1": { + "status": constants.UP, + "members": {"member-id-1": constants.UP}}, + "pool-id-1:listener-id-1": { + "status": constants.UP, + "members": {"member-id-1": constants.UP}}}, + "recv_time": time.time() + } + + lb_ref = self._make_fake_lb_health_dict() + self.amphora_repo.get_lb_for_health_update.return_value = lb_ref + self.hm.update_health(health, '192.0.2.1') + self.assertTrue(self.amphora_health_repo.replace.called) + + # test listener, member + for listener_id, listener in six.iteritems( + health.get('listeners', {})): + + self.listener_repo.update.assert_any_call( + self.session_mock, listener_id, + operating_status=constants.ONLINE) + + for pool_id, pool in six.iteritems(health.get('pools', {})): + # We should not double process a shared pool + self.hm.pool_repo.update.assert_called_once_with( + self.session_mock, 'pool-id-1', + operating_status=constants.ONLINE) + + for member_id, member in six.iteritems( + pool.get('members', {})): + self.member_repo.update.assert_any_call( + self.session_mock, member_id, + operating_status=constants.ONLINE) + def test_update_pool_offline(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-5": {"status": constants.UP, @@ -436,6 +532,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": { "status": constants.OPEN, @@ -475,6 +572,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": { "status": constants.OPEN, @@ -514,6 +612,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": { "status": constants.OPEN, @@ -547,6 +646,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -589,6 +689,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -626,6 +727,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -668,6 +770,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -710,6 +813,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": { "status": constants.OPEN, @@ -755,6 +859,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.FULL, "pools": { "pool-id-1": {"status": constants.UP, @@ -804,6 +909,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.DOWN, @@ -847,6 +953,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.FULL, "pools": { "pool-id-1": {"status": constants.DOWN, @@ -895,7 +1002,8 @@ class TestUpdateHealthDb(base.TestCase): lb_ref['listeners']['listener-id-%s' % i] = { constants.OPERATING_STATUS: 'bogus', - 'protocol': constants.PROTOCOL_TCP} + 'protocol': constants.PROTOCOL_TCP, + 'enabled': True} if i == 3: members_dict = {'member-id-3': { @@ -936,6 +1044,7 @@ class TestUpdateHealthDb(base.TestCase): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": {"status": constants.OPEN, "pools": { "pool-id-1": {"status": constants.UP, @@ -1000,6 +1109,7 @@ class TestUpdateHealthDb(base.TestCase): def test_update_health_no_status_change(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": { "listener-id-1": { "status": constants.OPEN, "pools": { @@ -1036,6 +1146,7 @@ class TestUpdateHealthDb(base.TestCase): def test_update_health_lb_admin_down(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time()} @@ -1118,10 +1229,12 @@ class TestUpdateHealthDb(base.TestCase): listener_protocol=constants.PROTOCOL_UDP) lb_ref['listeners']['listener-id-2'] = { constants.OPERATING_STATUS: 'bogus', - 'protocol': constants.PROTOCOL_UDP} + 'protocol': constants.PROTOCOL_UDP, + 'enabled': True} lb_ref['listeners']['listener-id-3'] = { constants.OPERATING_STATUS: 'bogus', - 'protocol': constants.PROTOCOL_UDP} + 'protocol': constants.PROTOCOL_UDP, + 'enabled': True} self.amphora_repo.get_lb_for_health_update.return_value = lb_ref self.hm.update_health(health, '192.0.2.1') @@ -1132,6 +1245,7 @@ class TestUpdateHealthDb(base.TestCase): def test_update_health_no_db_lb(self): health = { "id": self.FAKE_UUID_1, + "ver": 1, "listeners": {}, "recv_time": time.time() } @@ -1288,6 +1402,7 @@ class TestUpdateStatsDb(base.TestCase): health = { "id": self.amphora_id, + "ver": 1, "listeners": { self.listener_id: { "status": constants.OPEN, diff --git a/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py b/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py index c0789a4d27..19fea84da1 100644 --- a/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py +++ b/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py @@ -128,9 +128,10 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIsInstance(lb_flow, flow.Flow) self.assertIn(constants.LOADBALANCER, lb_flow.requires) + self.assertIn(constants.UPDATE_DICT, lb_flow.requires) self.assertEqual(0, len(lb_flow.provides)) - self.assertEqual(3, len(lb_flow.requires)) + self.assertEqual(2, len(lb_flow.requires)) def test_get_post_lb_amp_association_flow(self, mock_get_net_driver): amp_flow = self.LBFlow.get_post_lb_amp_association_flow( diff --git a/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py b/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py index 5bb70adfb6..93e6c7664b 100644 --- a/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py +++ b/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py @@ -93,15 +93,15 @@ class TestAmphoraDriverTasks(base.TestCase): constants.CONN_RETRY_INTERVAL: 4} amp_list_update_obj = amphora_driver_tasks.AmpListenersUpdate() - amp_list_update_obj.execute([_listener_mock], 0, + amp_list_update_obj.execute(_load_balancer_mock, 0, [_amphora_mock], timeout_dict) mock_driver.update_amphora_listeners.assert_called_once_with( - [_listener_mock], 0, [_amphora_mock], timeout_dict) + _load_balancer_mock, _amphora_mock, timeout_dict) mock_driver.update_amphora_listeners.side_effect = Exception('boom') - amp_list_update_obj.execute([_listener_mock], 0, + amp_list_update_obj.execute(_load_balancer_mock, 0, [_amphora_mock], timeout_dict) mock_amphora_repo_update.assert_called_once_with( @@ -117,9 +117,9 @@ class TestAmphoraDriverTasks(base.TestCase): mock_amphora_repo_update): listener_update_obj = amphora_driver_tasks.ListenersUpdate() - listener_update_obj.execute(_load_balancer_mock, [_listener_mock]) + listener_update_obj.execute(_load_balancer_mock) - mock_driver.update.assert_called_once_with(_listener_mock, _vip_mock) + mock_driver.update.assert_called_once_with(_load_balancer_mock) # Test the revert amp = listener_update_obj.revert(_load_balancer_mock) @@ -152,12 +152,9 @@ class TestAmphoraDriverTasks(base.TestCase): data_models.Listener(id='listener2')] vip = data_models.Vip(ip_address='10.0.0.1') lb = data_models.LoadBalancer(id='lb1', listeners=listeners, vip=vip) - listeners_update_obj.execute(lb, listeners) - mock_driver.update.assert_has_calls([mock.call(listeners[0], vip), - mock.call(listeners[1], vip)]) - self.assertEqual(2, mock_driver.update.call_count) - self.assertIsNotNone(listeners[0].load_balancer) - self.assertIsNotNone(listeners[1].load_balancer) + listeners_update_obj.execute(lb) + mock_driver.update.assert_called_once_with(lb) + self.assertEqual(1, mock_driver.update.call_count) # Test the revert amp = listeners_update_obj.revert(lb) @@ -171,69 +168,37 @@ class TestAmphoraDriverTasks(base.TestCase): self.assertEqual(2, repo.ListenerRepository.update.call_count) self.assertIsNone(amp) - def test_listener_stop(self, - mock_driver, - mock_generate_uuid, - mock_log, - mock_get_session, - mock_listener_repo_get, - mock_listener_repo_update, - mock_amphora_repo_update): + @mock.patch('octavia.controller.worker.task_utils.TaskUtils.' + 'mark_listener_prov_status_error') + def test_listeners_start(self, + mock_prov_status_error, + mock_driver, + mock_generate_uuid, + mock_log, + mock_get_session, + mock_listener_repo_get, + mock_listener_repo_update, + mock_amphora_repo_update): + listeners_start_obj = amphora_driver_tasks.ListenersStart() + mock_lb = mock.MagicMock() + mock_listener = mock.MagicMock() + mock_listener.id = '12345' - listener_stop_obj = amphora_driver_tasks.ListenerStop() - listener_stop_obj.execute(_load_balancer_mock, _listener_mock) + # Test no listeners + mock_lb.listeners = None + listeners_start_obj.execute(mock_lb) + mock_driver.start.assert_not_called() - mock_driver.stop.assert_called_once_with(_listener_mock, _vip_mock) + # Test with listeners + mock_driver.start.reset_mock() + mock_lb.listeners = [mock_listener] + listeners_start_obj.execute(mock_lb) + mock_driver.start.assert_called_once_with(mock_lb, None) - # Test the revert - amp = listener_stop_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - # Test the revert with exception - repo.ListenerRepository.update.reset_mock() - mock_listener_repo_update.side_effect = Exception('fail') - amp = listener_stop_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - def test_listener_start(self, - mock_driver, - mock_generate_uuid, - mock_log, - mock_get_session, - mock_listener_repo_get, - mock_listener_repo_update, - mock_amphora_repo_update): - - listener_start_obj = amphora_driver_tasks.ListenerStart() - listener_start_obj.execute(_load_balancer_mock, _listener_mock) - - mock_driver.start.assert_called_once_with(_listener_mock, _vip_mock) - - # Test the revert - amp = listener_start_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - # Test the revert with exception - repo.ListenerRepository.update.reset_mock() - mock_listener_repo_update.side_effect = Exception('fail') - amp = listener_start_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) + # Test revert + mock_lb.listeners = [mock_listener] + listeners_start_obj.revert(mock_lb) + mock_prov_status_error.assert_called_once_with('12345') def test_listener_delete(self, mock_driver, @@ -245,9 +210,9 @@ class TestAmphoraDriverTasks(base.TestCase): mock_amphora_repo_update): listener_delete_obj = amphora_driver_tasks.ListenerDelete() - listener_delete_obj.execute(_load_balancer_mock, _listener_mock) + listener_delete_obj.execute(_listener_mock) - mock_driver.delete.assert_called_once_with(_listener_mock, _vip_mock) + mock_driver.delete.assert_called_once_with(_listener_mock) # Test the revert amp = listener_delete_obj.revert(_listener_mock) diff --git a/octavia/tests/unit/controller/worker/v1/test_controller_worker.py b/octavia/tests/unit/controller/worker/v1/test_controller_worker.py index a8c3f989a0..a3780db235 100644 --- a/octavia/tests/unit/controller/worker/v1/test_controller_worker.py +++ b/octavia/tests/unit/controller/worker/v1/test_controller_worker.py @@ -49,6 +49,7 @@ _health_mon_mock = mock.MagicMock() _vip_mock = mock.MagicMock() _listener_mock = mock.MagicMock() _load_balancer_mock = mock.MagicMock() +_load_balancer_mock.listeners = [_listener_mock] _member_mock = mock.MagicMock() _pool_mock = mock.MagicMock() _l7policy_mock = mock.MagicMock() @@ -324,7 +325,7 @@ class TestControllerWorker(base.TestCase): store={constants.LOADBALANCER: _load_balancer_mock, constants.LISTENERS: - [_listener_mock]})) + _load_balancer_mock.listeners})) _flow_mock.run.assert_called_once_with() self.assertEqual(2, mock_listener_repo_get.call_count) diff --git a/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py b/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py index 8f0825ab15..d20c62db60 100644 --- a/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +++ b/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py @@ -128,9 +128,10 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIsInstance(lb_flow, flow.Flow) self.assertIn(constants.LOADBALANCER, lb_flow.requires) + self.assertIn(constants.UPDATE_DICT, lb_flow.requires) self.assertEqual(0, len(lb_flow.provides)) - self.assertEqual(3, len(lb_flow.requires)) + self.assertEqual(2, len(lb_flow.requires)) def test_get_post_lb_amp_association_flow(self, mock_get_net_driver): amp_flow = self.LBFlow.get_post_lb_amp_association_flow( diff --git a/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py b/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py index eb68c17e90..960b8e454c 100644 --- a/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py +++ b/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py @@ -93,15 +93,15 @@ class TestAmphoraDriverTasks(base.TestCase): constants.CONN_RETRY_INTERVAL: 4} amp_list_update_obj = amphora_driver_tasks.AmpListenersUpdate() - amp_list_update_obj.execute([_listener_mock], 0, + amp_list_update_obj.execute(_load_balancer_mock, 0, [_amphora_mock], timeout_dict) mock_driver.update_amphora_listeners.assert_called_once_with( - [_listener_mock], 0, [_amphora_mock], timeout_dict) + _load_balancer_mock, _amphora_mock, timeout_dict) mock_driver.update_amphora_listeners.side_effect = Exception('boom') - amp_list_update_obj.execute([_listener_mock], 0, + amp_list_update_obj.execute(_load_balancer_mock, 0, [_amphora_mock], timeout_dict) mock_amphora_repo_update.assert_called_once_with( @@ -117,9 +117,9 @@ class TestAmphoraDriverTasks(base.TestCase): mock_amphora_repo_update): listener_update_obj = amphora_driver_tasks.ListenersUpdate() - listener_update_obj.execute(_load_balancer_mock, [_listener_mock]) + listener_update_obj.execute(_load_balancer_mock) - mock_driver.update.assert_called_once_with(_listener_mock, _vip_mock) + mock_driver.update.assert_called_once_with(_load_balancer_mock) # Test the revert amp = listener_update_obj.revert(_load_balancer_mock) @@ -152,12 +152,9 @@ class TestAmphoraDriverTasks(base.TestCase): data_models.Listener(id='listener2')] vip = data_models.Vip(ip_address='10.0.0.1') lb = data_models.LoadBalancer(id='lb1', listeners=listeners, vip=vip) - listeners_update_obj.execute(lb, listeners) - mock_driver.update.assert_has_calls([mock.call(listeners[0], vip), - mock.call(listeners[1], vip)]) - self.assertEqual(2, mock_driver.update.call_count) - self.assertIsNotNone(listeners[0].load_balancer) - self.assertIsNotNone(listeners[1].load_balancer) + listeners_update_obj.execute(lb) + mock_driver.update.assert_called_once_with(lb) + self.assertEqual(1, mock_driver.update.call_count) # Test the revert amp = listeners_update_obj.revert(lb) @@ -171,69 +168,37 @@ class TestAmphoraDriverTasks(base.TestCase): self.assertEqual(2, repo.ListenerRepository.update.call_count) self.assertIsNone(amp) - def test_listener_stop(self, - mock_driver, - mock_generate_uuid, - mock_log, - mock_get_session, - mock_listener_repo_get, - mock_listener_repo_update, - mock_amphora_repo_update): + @mock.patch('octavia.controller.worker.task_utils.TaskUtils.' + 'mark_listener_prov_status_error') + def test_listeners_start(self, + mock_prov_status_error, + mock_driver, + mock_generate_uuid, + mock_log, + mock_get_session, + mock_listener_repo_get, + mock_listener_repo_update, + mock_amphora_repo_update): + listeners_start_obj = amphora_driver_tasks.ListenersStart() + mock_lb = mock.MagicMock() + mock_listener = mock.MagicMock() + mock_listener.id = '12345' - listener_stop_obj = amphora_driver_tasks.ListenerStop() - listener_stop_obj.execute(_load_balancer_mock, _listener_mock) + # Test no listeners + mock_lb.listeners = None + listeners_start_obj.execute(mock_lb) + mock_driver.start.assert_not_called() - mock_driver.stop.assert_called_once_with(_listener_mock, _vip_mock) + # Test with listeners + mock_driver.start.reset_mock() + mock_lb.listeners = [mock_listener] + listeners_start_obj.execute(mock_lb) + mock_driver.start.assert_called_once_with(mock_lb, None) - # Test the revert - amp = listener_stop_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - # Test the revert with exception - repo.ListenerRepository.update.reset_mock() - mock_listener_repo_update.side_effect = Exception('fail') - amp = listener_stop_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - def test_listener_start(self, - mock_driver, - mock_generate_uuid, - mock_log, - mock_get_session, - mock_listener_repo_get, - mock_listener_repo_update, - mock_amphora_repo_update): - - listener_start_obj = amphora_driver_tasks.ListenerStart() - listener_start_obj.execute(_load_balancer_mock, _listener_mock) - - mock_driver.start.assert_called_once_with(_listener_mock, _vip_mock) - - # Test the revert - amp = listener_start_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) - - # Test the revert with exception - repo.ListenerRepository.update.reset_mock() - mock_listener_repo_update.side_effect = Exception('fail') - amp = listener_start_obj.revert(_listener_mock) - repo.ListenerRepository.update.assert_called_once_with( - _session_mock, - id=LISTENER_ID, - provisioning_status=constants.ERROR) - self.assertIsNone(amp) + # Test revert + mock_lb.listeners = [mock_listener] + listeners_start_obj.revert(mock_lb) + mock_prov_status_error.assert_called_once_with('12345') def test_listener_delete(self, mock_driver, @@ -245,9 +210,9 @@ class TestAmphoraDriverTasks(base.TestCase): mock_amphora_repo_update): listener_delete_obj = amphora_driver_tasks.ListenerDelete() - listener_delete_obj.execute(_load_balancer_mock, _listener_mock) + listener_delete_obj.execute(_listener_mock) - mock_driver.delete.assert_called_once_with(_listener_mock, _vip_mock) + mock_driver.delete.assert_called_once_with(_listener_mock) # Test the revert amp = listener_delete_obj.revert(_listener_mock) diff --git a/octavia/tests/unit/controller/worker/v2/test_controller_worker.py b/octavia/tests/unit/controller/worker/v2/test_controller_worker.py index ef22478ce7..9c7a524467 100644 --- a/octavia/tests/unit/controller/worker/v2/test_controller_worker.py +++ b/octavia/tests/unit/controller/worker/v2/test_controller_worker.py @@ -49,6 +49,7 @@ _health_mon_mock = mock.MagicMock() _vip_mock = mock.MagicMock() _listener_mock = mock.MagicMock() _load_balancer_mock = mock.MagicMock() +_load_balancer_mock.listeners = [_listener_mock] _member_mock = mock.MagicMock() _pool_mock = mock.MagicMock() _l7policy_mock = mock.MagicMock() @@ -324,7 +325,7 @@ class TestControllerWorker(base.TestCase): store={constants.LOADBALANCER: _load_balancer_mock, constants.LISTENERS: - [_listener_mock]})) + _load_balancer_mock.listeners})) _flow_mock.run.assert_called_once_with() self.assertEqual(2, mock_listener_repo_get.call_count) diff --git a/releasenotes/notes/haproxy-single-process-b17a3af3a97accea.yaml b/releasenotes/notes/haproxy-single-process-b17a3af3a97accea.yaml new file mode 100644 index 0000000000..332912a3b3 --- /dev/null +++ b/releasenotes/notes/haproxy-single-process-b17a3af3a97accea.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - | + A new amphora image is required to resolve the amphora memory issues when + a load balancer has multiple listeners and the amphora image uses + haproxy 1.8 or newer. +fixes: + - | + Fixed an issue with load balancers that have multiple listeners when using + an amphora image that contains HAProxy 1.8 or newer. An updated amphora + image is required to apply this fix.