Merge "Fix multi-listener load balancers"

changes/08/661308/5
Zuul 3 years ago committed by Gerrit Code Review
commit 37799137a3
  1. 350
      doc/source/contributor/api/haproxy-amphora-api.rst
  2. 2
      octavia/amphorae/backends/agent/api_server/__init__.py
  3. 12
      octavia/amphorae/backends/agent/api_server/amphora_info.py
  4. 4
      octavia/amphorae/backends/agent/api_server/keepalived.py
  5. 44
      octavia/amphorae/backends/agent/api_server/keepalivedlvs.py
  6. 247
      octavia/amphorae/backends/agent/api_server/loadbalancer.py
  7. 74
      octavia/amphorae/backends/agent/api_server/server.py
  8. 11
      octavia/amphorae/backends/agent/api_server/udp_listener_base.py
  9. 106
      octavia/amphorae/backends/agent/api_server/util.py
  10. 77
      octavia/amphorae/backends/health_daemon/health_daemon.py
  11. 4
      octavia/amphorae/backends/utils/haproxy_query.py
  12. 72
      octavia/amphorae/drivers/driver_base.py
  13. 574
      octavia/amphorae/drivers/haproxy/rest_api_driver.py
  14. 15
      octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py
  15. 74
      octavia/amphorae/drivers/noop_driver/driver.py
  16. 3
      octavia/cmd/health_manager.py
  17. 3
      octavia/cmd/octavia_worker.py
  18. 0
      octavia/common/jinja/haproxy/combined_listeners/__init__.py
  19. 499
      octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py
  20. 52
      octavia/common/jinja/haproxy/combined_listeners/templates/base.j2
  21. 40
      octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2
  22. 370
      octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2
  23. 0
      octavia/common/jinja/haproxy/split_listeners/__init__.py
  24. 0
      octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py
  25. 0
      octavia/common/jinja/haproxy/split_listeners/templates/base.j2
  26. 0
      octavia/common/jinja/haproxy/split_listeners/templates/haproxy.cfg.j2
  27. 0
      octavia/common/jinja/haproxy/split_listeners/templates/macros.j2
  28. 98
      octavia/controller/healthmanager/health_drivers/update_db.py
  29. 3
      octavia/controller/worker/v1/controller_worker.py
  30. 5
      octavia/controller/worker/v1/flows/amphora_flows.py
  31. 6
      octavia/controller/worker/v1/flows/health_monitor_flows.py
  32. 6
      octavia/controller/worker/v1/flows/l7policy_flows.py
  33. 6
      octavia/controller/worker/v1/flows/l7rule_flows.py
  34. 8
      octavia/controller/worker/v1/flows/listener_flows.py
  35. 2
      octavia/controller/worker/v1/flows/load_balancer_flows.py
  36. 8
      octavia/controller/worker/v1/flows/member_flows.py
  37. 6
      octavia/controller/worker/v1/flows/pool_flows.py
  38. 59
      octavia/controller/worker/v1/tasks/amphora_driver_tasks.py
  39. 3
      octavia/controller/worker/v2/controller_worker.py
  40. 5
      octavia/controller/worker/v2/flows/amphora_flows.py
  41. 6
      octavia/controller/worker/v2/flows/health_monitor_flows.py
  42. 6
      octavia/controller/worker/v2/flows/l7policy_flows.py
  43. 6
      octavia/controller/worker/v2/flows/l7rule_flows.py
  44. 8
      octavia/controller/worker/v2/flows/listener_flows.py
  45. 2
      octavia/controller/worker/v2/flows/load_balancer_flows.py
  46. 8
      octavia/controller/worker/v2/flows/member_flows.py
  47. 6
      octavia/controller/worker/v2/flows/pool_flows.py
  48. 59
      octavia/controller/worker/v2/tasks/amphora_driver_tasks.py
  49. 4
      octavia/db/repositories.py
  50. 62
      octavia/tests/functional/amphorae/backend/agent/api_server/test_keepalivedlvs.py
  51. 300
      octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py
  52. 3
      octavia/tests/functional/db/test_repositories.py
  53. 43
      octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py
  54. 14
      octavia/tests/unit/amphorae/backends/agent/api_server/test_haproxy_compatibility.py
  55. 8
      octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py
  56. 230
      octavia/tests/unit/amphorae/backends/agent/api_server/test_loadbalancer.py
  57. 104
      octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py
  58. 50
      octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py
  59. 49
      octavia/tests/unit/amphorae/backends/utils/test_haproxy_query.py
  60. 649
      octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py
  61. 1299
      octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py
  62. 22
      octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py
  63. 29
      octavia/tests/unit/amphorae/drivers/test_noop_amphoraloadbalancer_driver.py
  64. 3
      octavia/tests/unit/certificates/manager/test_barbican.py
  65. 0
      octavia/tests/unit/common/jinja/haproxy/combined_listeners/__init__.py
  66. 1205
      octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py
  67. 0
      octavia/tests/unit/common/jinja/haproxy/split_listeners/__init__.py
  68. 298
      octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py
  69. 55
      octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py
  70. 1076
      octavia/tests/unit/common/sample_configs/sample_configs_combined.py
  71. 58
      octavia/tests/unit/common/sample_configs/sample_configs_split.py
  72. 12
      octavia/tests/unit/common/tls_utils/test_cert_parser.py
  73. 127
      octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py
  74. 3
      octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py
  75. 115
      octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py
  76. 3
      octavia/tests/unit/controller/worker/v1/test_controller_worker.py
  77. 3
      octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py
  78. 115
      octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py
  79. 3
      octavia/tests/unit/controller/worker/v2/test_controller_worker.py
  80. 11
      releasenotes/notes/haproxy-single-process-b17a3af3a97accea.yaml

@ -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
-----------------------
* **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:
::
Start or Stop a load balancer
-----------------------------
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:

@ -12,4 +12,4 @@
# License for the specific language governing permissions and limitations
# under the License.
VERSION = '0.5'
VERSION = '1.0'

@ -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

@ -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())

@ -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)

@ -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 <peername>" 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 = ''
if status == consts.ACTIVE:
listener_type = self._parse_haproxy_file(listener)['mode']
for lb in util.get_loadbalancers():
stats_socket, listeners_on_lb = util.parse_haproxy_file(lb)
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