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 <flux.adam@gmail.com>
Change-Id: Idaccbcfa0126f1e26fbb3ad770c65c9266cfad5b
changes/68/668068/33
Michael Johnson 2019-06-27 16:00:22 -07:00
parent 31428bf25a
commit 06ce4777c3
80 changed files with 6752 additions and 2136 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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