Merge "Amphora Flows and Drivers for Active Standby"
This commit is contained in:
@@ -369,6 +369,80 @@ health of the amphora, currently-configured topology and role, etc.
|
||||
],
|
||||
}
|
||||
|
||||
Get interface
|
||||
-------------
|
||||
|
||||
* **URL:** /*:version*/interface/*:ip*
|
||||
* **Method:** GET
|
||||
* **URL params:**
|
||||
|
||||
* *:ip* = the ip address to find the interface name
|
||||
|
||||
* **Data params:** none
|
||||
* **Success Response:**
|
||||
|
||||
* Code: 200
|
||||
|
||||
* Content: OK
|
||||
* Content: JSON formatted interface
|
||||
|
||||
* **Error Response:**
|
||||
|
||||
* Code: 400
|
||||
|
||||
* Content: Bad IP address version
|
||||
|
||||
* Code: 404
|
||||
|
||||
* Content: Error interface not found for IP address
|
||||
|
||||
* **Response:**
|
||||
|
||||
| OK
|
||||
| eth1
|
||||
|
||||
**Examples:**
|
||||
|
||||
* Success code 200:
|
||||
|
||||
::
|
||||
|
||||
GET URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/interface/10.0.0.1
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'OK',
|
||||
'interface': 'eth1'
|
||||
}
|
||||
|
||||
|
||||
* Error code 404:
|
||||
|
||||
::
|
||||
|
||||
GET URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/interface/10.5.0.1
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'Error interface not found for IP address',
|
||||
}
|
||||
|
||||
|
||||
* Error code 404:
|
||||
|
||||
::
|
||||
|
||||
GET URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/interface/10.6.0.1.1
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'Bad IP address version',
|
||||
}
|
||||
|
||||
|
||||
Get all listeners' statuses
|
||||
---------------------------
|
||||
|
||||
@@ -1336,3 +1410,116 @@ not be available for soem time.
|
||||
}
|
||||
|
||||
|
||||
Upload keepalived configuration
|
||||
-------------------------------
|
||||
|
||||
* **URL:** /*:version*/vrrp/upload
|
||||
* **Method:** PUT
|
||||
* **URL params:** none
|
||||
* **Data params:** none
|
||||
* **Success Response:**
|
||||
|
||||
* Code: 200
|
||||
|
||||
* Content: OK
|
||||
|
||||
* **Error Response:**
|
||||
|
||||
* Code: 500
|
||||
|
||||
* Content: Failed to upload keepalived configuration.
|
||||
|
||||
* **Response:**
|
||||
|
||||
OK
|
||||
|
||||
**Examples:**
|
||||
|
||||
* Success code 200:
|
||||
|
||||
::
|
||||
|
||||
PUT URI:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/vrrp/upload
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'OK'
|
||||
}
|
||||
|
||||
|
||||
Start, Stop, or Reload keepalived
|
||||
---------------------------------
|
||||
|
||||
* **URL:** /*:version*/vrrp/*:action*
|
||||
* **Method:** PUT
|
||||
* **URL params:**
|
||||
|
||||
* *:action* = One of: start, stop, reload
|
||||
|
||||
* **Data params:** none
|
||||
* **Success Response:**
|
||||
|
||||
* Code: 202
|
||||
|
||||
* Content: OK
|
||||
|
||||
* **Error Response:**
|
||||
|
||||
* Code: 400
|
||||
|
||||
* Content: Invalid Request
|
||||
|
||||
* Code: 500
|
||||
|
||||
* Content: Failed to start / stop / reload keepalived service:
|
||||
* *(Also contains error output from attempt to start / stop / \
|
||||
reload keepalived)*
|
||||
|
||||
* **Response:**
|
||||
|
||||
| OK
|
||||
| keepalived started
|
||||
|
||||
**Examples:**
|
||||
|
||||
* Success code 202:
|
||||
|
||||
::
|
||||
|
||||
PUT URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/vrrp/start
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'OK',
|
||||
'details': 'keepalived started',
|
||||
}
|
||||
|
||||
* Error code: 400
|
||||
|
||||
::
|
||||
|
||||
PUT URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/vrrp/BAD_TEST_DATA
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'Invalid Request',
|
||||
'details': 'Unknown action: BAD_TEST_DATA',
|
||||
}
|
||||
|
||||
* Error code: 500
|
||||
|
||||
::
|
||||
|
||||
PUT URL:
|
||||
https://octavia-haproxy-img-00328.local/v0.1/vrrp/stop
|
||||
|
||||
JSON Response:
|
||||
{
|
||||
'message': 'Failed to stop keepalived service: keeepalived process with PID 3352 not found',
|
||||
'details': 'keeepalived process with PID 3352 not found',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# This is temporary until we have a pip package
|
||||
amphora-agent git /opt/amphora-agent https://review.openstack.org/openstack/octavia
|
||||
amphora-agent git /opt/amphora-agent https://review.openstack.org/openstack/octavia refs/changes/52/206252/52
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
|
||||
#Checks out keepalived version 1.2.13, compiles and installs binaries.
|
||||
# install keepalived dependances.
|
||||
apt-get --assume-yes install `apt-cache depends keepalived | awk '/Depends:/{print$2}'`
|
||||
# Checks out keepalived version 1.2.19, compiles and installs binaries.
|
||||
cd /opt/vrrp-octavia/
|
||||
git checkout v1.2.13
|
||||
git checkout v1.2.19
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Clone source for keepalived version 1.2.13. Correct version is in the installation script
|
||||
# Clone source for keepalived version 1.2.19. Correct version is in
|
||||
# the installation script
|
||||
vrrp-octavia git /opt/vrrp-octavia https://github.com/acassen/keepalived
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
# Change for production to a ram drive
|
||||
# haproxy_cert_dir = /tmp
|
||||
|
||||
# Maximum number of entries that can fit in the stick table.
|
||||
# The size supports "k", "m", "g" suffixes.
|
||||
# haproxy_stick_table_size = 10k
|
||||
|
||||
[controller_worker]
|
||||
# amp_active_retries = 10
|
||||
# amp_active_wait_sec = 10
|
||||
@@ -145,6 +149,9 @@
|
||||
# barbican_cert_generator
|
||||
# anchor_cert_generator
|
||||
# cert_generator = local_cert_generator
|
||||
#
|
||||
# Load balancer topology options are SINGLE, ACTIVE_STANDBY
|
||||
# loadbalancer_topology = SINGLE
|
||||
|
||||
[task_flow]
|
||||
# engine = serial
|
||||
@@ -182,3 +189,16 @@
|
||||
# agent_server_cert = /etc/octavia/certs/server.pem
|
||||
# agent_server_network_dir = /etc/network/interfaces.d/
|
||||
# agent_server_network_file =
|
||||
|
||||
[keepalived_vrrp]
|
||||
# Amphora Role/Priority advertisement interval in seconds
|
||||
# vrrp_advert_int = 1
|
||||
|
||||
# Service health check interval and success/fail count
|
||||
# vrrp_check_interval = 5
|
||||
# vrpp_fail_count = 2
|
||||
# vrrp_success_count = 2
|
||||
|
||||
# Amphora MASTER gratuitous ARP refresh settings
|
||||
# vrrp_garp_refresh_interval = 5
|
||||
# vrrp_garp_refresh_count = 2
|
||||
|
||||
@@ -19,7 +19,9 @@ import socket
|
||||
import subprocess
|
||||
|
||||
import flask
|
||||
import ipaddress
|
||||
import netifaces
|
||||
import six
|
||||
|
||||
from octavia.amphorae.backends.agent import api_server
|
||||
from octavia.amphorae.backends.agent.api_server import util
|
||||
@@ -138,3 +140,27 @@ def _get_networks():
|
||||
network_tx=_get_network_bytes(interface, 'tx'),
|
||||
network_rx=_get_network_bytes(interface, 'rx'))
|
||||
return networks
|
||||
|
||||
|
||||
def get_interface(ip_addr):
|
||||
if six.PY2:
|
||||
ip_version = ipaddress.ip_address(unicode(ip_addr)).version
|
||||
else:
|
||||
ip_version = ipaddress.ip_address(ip_addr).version
|
||||
if ip_version == 4:
|
||||
address_format = netifaces.AF_INET
|
||||
elif ip_version == 6:
|
||||
address_format = netifaces.AF_INET6
|
||||
else:
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message="Bad IP address version")), 400)
|
||||
for interface in netifaces.interfaces():
|
||||
for i in netifaces.ifaddresses(interface)[address_format]:
|
||||
if i['addr'] == ip_addr:
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message='OK', interface=interface)),
|
||||
200)
|
||||
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message="Error interface not found "
|
||||
"for IP address")), 404)
|
||||
|
||||
115
octavia/amphorae/backends/agent/api_server/keepalived.py
Normal file
115
octavia/amphorae/backends/agent/api_server/keepalived.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import flask
|
||||
import jinja2
|
||||
|
||||
from octavia.amphorae.backends.agent.api_server import listener
|
||||
from octavia.amphorae.backends.agent.api_server import util
|
||||
from octavia.common import constants as consts
|
||||
|
||||
|
||||
BUFFER = 100
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
||||
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
|
||||
template = j2_env.get_template(consts.KEEPALIVED_CONF)
|
||||
check_script_template = j2_env.get_template(consts.CHECK_SCRIPT_CONF)
|
||||
|
||||
|
||||
def upload_keepalived_config():
|
||||
stream = listener.Wrapped(flask.request.stream)
|
||||
|
||||
if not os.path.exists(util.keepalived_dir()):
|
||||
os.makedirs(util.keepalived_dir())
|
||||
os.makedirs(util.keepalived_check_scripts_dir())
|
||||
|
||||
conf_file = util.keepalived_cfg_path()
|
||||
with open(conf_file, 'w') as f:
|
||||
b = stream.read(BUFFER)
|
||||
while b:
|
||||
f.write(b)
|
||||
b = stream.read(BUFFER)
|
||||
|
||||
if not os.path.exists(util.keepalived_init_path()):
|
||||
with open(util.keepalived_init_path(), 'w') as text_file:
|
||||
text = template.render(
|
||||
keepalived_pid=util.keepalived_pid_path(),
|
||||
keepalived_cmd=consts.KEEPALIVED_CMD,
|
||||
keepalived_cfg=util.keepalived_cfg_path(),
|
||||
keepalived_log=util.keepalived_log_path()
|
||||
)
|
||||
text_file.write(text)
|
||||
cmd = "chmod +x {file}".format(file=util.keepalived_init_path())
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.debug("Failed to upload keepalived configuration. "
|
||||
"Unable to chmod init script.")
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="Failed to upload keepalived configuration. "
|
||||
"Unable to chmod init script.",
|
||||
details=e.output)), 500)
|
||||
# Renders the Keepalived check script
|
||||
with open(util.keepalived_check_script_path(), 'w') as text_file:
|
||||
text = check_script_template.render(
|
||||
check_scripts_dir=util.keepalived_check_scripts_dir()
|
||||
)
|
||||
text_file.write(text)
|
||||
cmd = ("chmod +x {file}".format(
|
||||
file=util.keepalived_check_script_path()))
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.debug("Failed to upload keepalived configuration. "
|
||||
"Unable to chmod check script.")
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="Failed to upload keepalived configuration. "
|
||||
"Unable to chmod check script.",
|
||||
details=e.output)), 500)
|
||||
|
||||
res = flask.make_response(flask.jsonify({
|
||||
'message': 'OK'}), 200)
|
||||
res.headers['ETag'] = stream.get_md5()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def manager_keepalived_service(action):
|
||||
action = action.lower()
|
||||
if action not in ['start', 'stop', 'reload']:
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message='Invalid Request',
|
||||
details="Unknown action: {0}".format(action))), 400)
|
||||
|
||||
cmd = ("/usr/sbin/service octavia-keepalived {action}".format(
|
||||
action=action))
|
||||
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.debug("Failed to {0} keepalived service: {1}".format(action, e))
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="Failed to {0} keepalived service".format(action),
|
||||
details=e.output)), 500)
|
||||
|
||||
return flask.make_response(flask.jsonify(
|
||||
dict(message='OK',
|
||||
details='keepalived {action}ed'.format(action=action))), 202)
|
||||
@@ -28,6 +28,7 @@ from werkzeug import exceptions
|
||||
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
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
BUFFER = 100
|
||||
@@ -78,13 +79,16 @@ def get_haproxy_config(listener_id):
|
||||
|
||||
"""Upload the haproxy config
|
||||
|
||||
:param amphora_id: The id of the amphora to update
|
||||
:param listener_id: The id of the listener
|
||||
"""
|
||||
|
||||
|
||||
def upload_haproxy_config(listener_id):
|
||||
def upload_haproxy_config(amphora_id, listener_id):
|
||||
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))
|
||||
|
||||
@@ -96,7 +100,8 @@ def upload_haproxy_config(listener_id):
|
||||
b = stream.read(BUFFER)
|
||||
|
||||
# use haproxy to check the config
|
||||
cmd = "haproxy -c -f {config_file}".format(config_file=name)
|
||||
cmd = "haproxy -c -L {peer} -f {config_file}".format(config_file=name,
|
||||
peer=peer_name)
|
||||
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
@@ -113,6 +118,7 @@ def upload_haproxy_config(listener_id):
|
||||
if not os.path.exists(util.upstart_path(listener_id)):
|
||||
with open(util.upstart_path(listener_id), 'w') as text_file:
|
||||
text = template.render(
|
||||
peer_name=peer_name,
|
||||
haproxy_pid=util.pid_path(listener_id),
|
||||
haproxy_cmd=util.CONF.haproxy_amphora.haproxy_cmd,
|
||||
haproxy_cfg=util.config_path(listener_id),
|
||||
@@ -136,17 +142,24 @@ def start_stop_listener(listener_id, action):
|
||||
|
||||
_check_listener_exists(listener_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()):
|
||||
vrrp_check_script_update(listener_id, action)
|
||||
|
||||
cmd = ("/usr/sbin/service haproxy-{listener_id} {action}".format(
|
||||
listener_id=listener_id, action=action))
|
||||
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.debug("Failed to %(action)s HAProxy service: %(err)s",
|
||||
{'action': action, 'err': e})
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="Error {0}ing haproxy".format(action),
|
||||
details=e.output)), 500)
|
||||
if 'Job is already running' not in e.output:
|
||||
LOG.debug("Failed to %(action)s HAProxy service: %(err)s",
|
||||
{'action': action, 'err': e})
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="Error {0}ing haproxy".format(action),
|
||||
details=e.output)), 500)
|
||||
if action in ['stop', 'reload']:
|
||||
return flask.make_response(flask.jsonify(
|
||||
dict(message='OK',
|
||||
@@ -369,3 +382,20 @@ def _cert_dir(listener_id):
|
||||
|
||||
def _cert_file_path(listener_id, filename):
|
||||
return os.path.join(_cert_dir(listener_id), filename)
|
||||
|
||||
|
||||
def vrrp_check_script_update(listener_id, action):
|
||||
listener_ids = util.get_listeners()
|
||||
if action == 'stop':
|
||||
listener_ids.remove(listener_id)
|
||||
args = []
|
||||
for listener_id in listener_ids:
|
||||
args.append(util.haproxy_sock_path(listener_id))
|
||||
|
||||
if not os.path.exists(util.keepalived_dir()):
|
||||
os.makedirs(util.keepalived_dir())
|
||||
os.makedirs(util.keepalived_check_scripts_dir())
|
||||
|
||||
cmd = 'haproxy-vrrp-check {args}; exit $?'.format(args=' '.join(args))
|
||||
with open(util.haproxy_check_script_path(), 'w') as text_file:
|
||||
text_file.write(cmd)
|
||||
|
||||
@@ -21,6 +21,7 @@ from werkzeug import exceptions
|
||||
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 plug
|
||||
|
||||
@@ -42,12 +43,11 @@ for code in six.iterkeys(exceptions.default_exceptions):
|
||||
app.error_handler_spec[None][code] = make_json_error
|
||||
|
||||
|
||||
# Tested with curl -k -XPUT --data-binary @/tmp/test.txt
|
||||
# https://127.0.0.1:9443/0.5/listeners/123/haproxy
|
||||
@app.route('/' + api_server.VERSION + '/listeners/<listener_id>/haproxy',
|
||||
@app.route('/' + api_server.VERSION +
|
||||
'/listeners/<amphora_id>/<listener_id>/haproxy',
|
||||
methods=['PUT'])
|
||||
def upload_haproxy_config(listener_id):
|
||||
return listener.upload_haproxy_config(listener_id)
|
||||
def upload_haproxy_config(amphora_id, listener_id):
|
||||
return listener.upload_haproxy_config(amphora_id, listener_id)
|
||||
|
||||
|
||||
@app.route('/' + api_server.VERSION + '/listeners/<listener_id>/haproxy',
|
||||
@@ -142,3 +142,18 @@ def plug_network():
|
||||
@app.route('/' + api_server.VERSION + '/certificate', methods=['PUT'])
|
||||
def upload_cert():
|
||||
return certificate_update.upload_server_cert()
|
||||
|
||||
|
||||
@app.route('/' + api_server.VERSION + '/vrrp/upload', methods=['PUT'])
|
||||
def upload_vrrp_config():
|
||||
return keepalived.upload_keepalived_config()
|
||||
|
||||
|
||||
@app.route('/' + api_server.VERSION + '/vrrp/<action>', methods=['PUT'])
|
||||
def manage_service_vrrp(action):
|
||||
return keepalived.manager_keepalived_service(action)
|
||||
|
||||
|
||||
@app.route('/' + api_server.VERSION + '/interface/<ip_addr>', methods=['GET'])
|
||||
def get_interface(ip_addr):
|
||||
return amphora_info.get_interface(ip_addr)
|
||||
|
||||
@@ -23,6 +23,7 @@ start on startup
|
||||
env PID_PATH={{ haproxy_pid }}
|
||||
env BIN_PATH={{ haproxy_cmd }}
|
||||
env CONF_PATH={{ haproxy_cfg }}
|
||||
env PEER_NAME={{ peer_name }}
|
||||
|
||||
respawn
|
||||
respawn limit {{ respawn_count }} {{respawn_interval}}
|
||||
@@ -34,9 +35,10 @@ end script
|
||||
script
|
||||
exec /bin/bash <<EOF
|
||||
echo \$(date) Starting HAProxy
|
||||
$BIN_PATH -f $CONF_PATH -D -p $PID_PATH
|
||||
# The -L trick fixes the HAProxy limitation to have long peer names
|
||||
$BIN_PATH -f $CONF_PATH -L $PEER_NAME -D -p $PID_PATH
|
||||
|
||||
trap "$BIN_PATH -f $CONF_PATH -p $PID_PATH -sf \\\$(cat $PID_PATH)" SIGHUP
|
||||
trap "$BIN_PATH -f $CONF_PATH -L $PEER_NAME -p $PID_PATH -sf \\\$(cat $PID_PATH)" SIGHUP
|
||||
trap "kill -TERM \\\$(cat $PID_PATH) && rm $PID_PATH;echo \\\$(date) Exiting HAProxy; exit 0" SIGTERM SIGINT
|
||||
|
||||
while true; do # Iterate to keep job running.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{#
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#}
|
||||
#!/bin/sh
|
||||
|
||||
RETVAL=0
|
||||
|
||||
prog="octavia-keepalived"
|
||||
|
||||
start() {
|
||||
echo -n $"Starting $prog"
|
||||
{{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }}
|
||||
RETVAL=$?
|
||||
echo
|
||||
[ $RETVAL -eq 0 ] && touch {{ keepalived_pid }}
|
||||
}
|
||||
|
||||
stop() {
|
||||
echo -n $"Stopping $prog"
|
||||
kill -9 `pidof keepalived`
|
||||
RETVAL=$?
|
||||
echo
|
||||
[ $RETVAL -eq 0 ] && rm -f {{ keepalived_pid }}
|
||||
}
|
||||
|
||||
status() {
|
||||
kill -0 `pidof keepalived`
|
||||
RETVAL=$?
|
||||
[ $RETVAL -eq 0 ] && echo -n $"$prog is running"
|
||||
[ $RETVAL -eq 1 ] && echo -n $"$prog is not found"
|
||||
echo
|
||||
}
|
||||
|
||||
# See how we were called.
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
reload)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|reload|status}"
|
||||
RETVAL=1
|
||||
esac
|
||||
|
||||
exit $RETVAL
|
||||
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#}
|
||||
#!/bin/sh
|
||||
|
||||
status=0
|
||||
for file in {{ check_scripts_dir }}/*
|
||||
do
|
||||
echo "Running check script: " $file
|
||||
sh $file
|
||||
status=$(( $status + $? ))
|
||||
done
|
||||
exit $status
|
||||
@@ -21,6 +21,7 @@ CONF = cfg.CONF
|
||||
CONF.import_group('amphora_agent', 'octavia.common.config')
|
||||
CONF.import_group('haproxy_amphora', 'octavia.common.config')
|
||||
UPSTART_DIR = '/etc/init'
|
||||
KEEPALIVED_INIT_DIR = '/etc/init.d'
|
||||
|
||||
|
||||
def upstart_path(listener_id):
|
||||
@@ -44,6 +45,48 @@ def get_haproxy_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_check_script_path():
|
||||
return os.path.join(keepalived_check_scripts_dir(),
|
||||
'haproxy_check_script.sh')
|
||||
|
||||
|
||||
def keepalived_dir():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path, 'vrrp')
|
||||
|
||||
|
||||
def keepalived_init_path():
|
||||
return os.path.join(KEEPALIVED_INIT_DIR, 'octavia-keepalived')
|
||||
|
||||
|
||||
def keepalived_pid_path():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path,
|
||||
'vrrp/octavia-keepalived.pid')
|
||||
|
||||
|
||||
def keepalived_cfg_path():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path,
|
||||
'vrrp/octavia-keepalived.conf')
|
||||
|
||||
|
||||
def keepalived_log_path():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path,
|
||||
'vrrp/octavia-keepalived.log')
|
||||
|
||||
|
||||
def keepalived_check_scripts_dir():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path,
|
||||
'vrrp/check_scripts')
|
||||
|
||||
|
||||
def keepalived_check_script_path():
|
||||
return os.path.join(CONF.haproxy_amphora.base_path,
|
||||
'vrrp/check_script.sh')
|
||||
|
||||
|
||||
"""Get Listeners
|
||||
|
||||
:returns An array with the ids of all listeners, e.g. ['123', '456', ...]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright 2011-2014 OpenStack Foundation,author: Min Wang,German Eichberger
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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
|
||||
@@ -242,3 +243,44 @@ class StatsMixin(object):
|
||||
awesome update code and code to send to ceilometer
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class VRRPDriverMixin(object):
|
||||
"""Abstract mixin class for VRRP support in loadbalancer amphorae
|
||||
|
||||
Usage: To plug VRRP support in another service driver XYZ, use:
|
||||
@plug_mixin(XYZ)
|
||||
class XYZ: ...
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def update_vrrp_conf(self, loadbalancer):
|
||||
"""Update amphorae of the loadbalancer with a new VRRP configuration
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def stop_vrrp_service(self, loadbalancer):
|
||||
"""Stop the vrrp services running on the loadbalancer's amphorae
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def start_vrrp_service(self, loadbalancer):
|
||||
"""Start the VRRP services of all amphorae of the loadbalancer
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reload_vrrp_service(self, loadbalancer):
|
||||
"""Reload the VRRP services of all amphorae of the loadbalancer
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -19,8 +19,9 @@ from webob import exc
|
||||
def check_exception(response):
|
||||
status_code = response.status_code
|
||||
responses = {
|
||||
400: InvalidRequest,
|
||||
401: Unauthorized,
|
||||
403: InvalidRequest,
|
||||
403: Forbidden,
|
||||
404: NotFound,
|
||||
405: InvalidRequest,
|
||||
409: Conflict,
|
||||
@@ -42,13 +43,18 @@ class APIException(exc.HTTPClientError):
|
||||
super(APIException, self).__init__(detail=self.msg)
|
||||
|
||||
|
||||
class InvalidRequest(APIException):
|
||||
msg = "Invalid request"
|
||||
code = 400
|
||||
|
||||
|
||||
class Unauthorized(APIException):
|
||||
msg = "Unauthorized"
|
||||
code = 401
|
||||
|
||||
|
||||
class InvalidRequest(APIException):
|
||||
msg = "Invalid request"
|
||||
class Forbidden(APIException):
|
||||
msg = "Forbidden"
|
||||
code = 403
|
||||
|
||||
|
||||
@@ -69,4 +75,4 @@ class InternalServerError(APIException):
|
||||
|
||||
class ServiceUnavailable(APIException):
|
||||
msg = "Service Unavailable"
|
||||
code = 503
|
||||
code = 503
|
||||
|
||||
@@ -17,7 +17,9 @@ import os
|
||||
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',
|
||||
@@ -42,6 +44,9 @@ HAPROXY_TEMPLATE = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'templates/haproxy_listener.template'))
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('haproxy_amphora', 'octavia.common.config')
|
||||
|
||||
JINJA_ENV = None
|
||||
|
||||
|
||||
@@ -104,6 +109,7 @@ class JinjaTemplater(object):
|
||||
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 render_loadbalancer_obj(self, listener,
|
||||
@@ -144,7 +150,8 @@ class JinjaTemplater(object):
|
||||
return {
|
||||
'name': loadbalancer.name,
|
||||
'vip_address': loadbalancer.vip.ip_address,
|
||||
'listener': listener
|
||||
'listener': listener,
|
||||
'topology': loadbalancer.topology
|
||||
}
|
||||
|
||||
def _transform_listener(self, listener, tls_cert):
|
||||
@@ -156,7 +163,10 @@ class JinjaTemplater(object):
|
||||
'id': listener.id,
|
||||
'protocol_port': listener.protocol_port,
|
||||
'protocol_mode': PROTOCOL_MAP[listener.protocol],
|
||||
'protocol': listener.protocol
|
||||
'protocol': listener.protocol,
|
||||
'peer_port': listener.peer_port,
|
||||
'topology': listener.load_balancer.topology,
|
||||
'amphorae': listener.load_balancer.amphorae
|
||||
}
|
||||
if listener.connection_limit and listener.connection_limit > -1:
|
||||
ret_value['connection_limit'] = listener.connection_limit
|
||||
@@ -185,7 +195,8 @@ class JinjaTemplater(object):
|
||||
'health_monitor': '',
|
||||
'session_persistence': '',
|
||||
'enabled': pool.enabled,
|
||||
'operating_status': pool.operating_status
|
||||
'operating_status': pool.operating_status,
|
||||
'stick_size': CONF.haproxy_amphora.haproxy_stick_size
|
||||
}
|
||||
members = [self._transform_member(x) for x in pool.members]
|
||||
ret_value['members'] = members
|
||||
|
||||
@@ -30,4 +30,6 @@ defaults
|
||||
timeout client {{ timeout_client | default('50000', true) }}
|
||||
timeout server {{ timeout_server | default('50000', true) }}
|
||||
|
||||
{% block peers %}{% endblock peers %}
|
||||
|
||||
{% block proxies %}{% endblock proxies %}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
{% set usergroup = user_group %}
|
||||
{% set sock_path = stats_sock %}
|
||||
|
||||
{% block peers %}
|
||||
{% from 'haproxy_proxies.template' import peers_macro%}
|
||||
{{ peers_macro(constants, loadbalancer.listener) }}
|
||||
{% endblock peers %}
|
||||
|
||||
{% block proxies %}
|
||||
{% from 'haproxy_proxies.template' import frontend_macro as frontend_macro, backend_macro%}
|
||||
{{ frontend_macro(constants, loadbalancer.listener, loadbalancer.vip_address) }}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
{% set usergroup = user_group %}
|
||||
{% set sock_path = stats_sock %}
|
||||
|
||||
{% block peers %}
|
||||
{% from 'haproxy_proxies.template' import peers_macro%}
|
||||
{{ peers_macro(constants, loadbalancer.listener) }}
|
||||
{% endblock peers %}
|
||||
|
||||
{% block proxies %}
|
||||
{% from 'haproxy_proxies.template' import frontend_macro as frontend_macro, backend_macro%}
|
||||
{% for listener in loadbalancer.listeners %}
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
#}
|
||||
{% extends 'haproxy_base.template' %}
|
||||
|
||||
{% macro peers_macro(constants,listener) %}
|
||||
{% if listener.topology == constants.TOPOLOGY_ACTIVE_STANDBY %}
|
||||
peers {{ "%s_peers"|format(listener.id.replace("-", ""))|trim() }}
|
||||
{% for amp in listener.amphorae %}
|
||||
{# HAProxy has peer name limitations, thus the hash filter #}
|
||||
peer {{ amp.id|hash_amp_id|replace('=', '') }} {{ amp.vrrp_ip }}:{{ listener.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() %}
|
||||
@@ -60,7 +70,11 @@ backend {{ pool.id }}
|
||||
{% endif %}
|
||||
{% if pool.session_persistence %}
|
||||
{% if pool.session_persistence.type == constants.SESSION_PERSISTENCE_SOURCE_IP %}
|
||||
stick-table type ip size 10k
|
||||
{% if listener.topology == constants.TOPOLOGY_ACTIVE_STANDBY %}
|
||||
stick-table type ip size {{ pool.stick_size }} peers {{ "%s_peers"|format(listener.id.replace("-", ""))|trim() }}
|
||||
{% else %}
|
||||
stick-table type ip size {{ pool.stick_size }}
|
||||
{% endif %}
|
||||
stick on src
|
||||
{% elif pool.session_persistence.type == constants.SESSION_PERSISTENCE_HTTP_COOKIE %}
|
||||
cookie SRV insert indirect nocache
|
||||
|
||||
@@ -26,13 +26,14 @@ from octavia.amphorae.driver_exceptions import exceptions as driver_except
|
||||
from octavia.amphorae.drivers import driver_base as driver_base
|
||||
from octavia.amphorae.drivers.haproxy import exceptions as exc
|
||||
from octavia.amphorae.drivers.haproxy.jinja import jinja_cfg
|
||||
from octavia.amphorae.drivers.keepalived import vrrp_rest_driver
|
||||
from octavia.common.config import cfg
|
||||
from octavia.common import constants
|
||||
from octavia.common.tls_utils import cert_parser
|
||||
from octavia.i18n import _LW
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
API_VERSION = '0.5'
|
||||
API_VERSION = constants.API_VERSION
|
||||
OCTAVIA_API_CLIENT = (
|
||||
"Octavia HaProxy Rest Client/{version} "
|
||||
"(https://wiki.openstack.org/wiki/Octavia)").format(version=API_VERSION)
|
||||
@@ -40,7 +41,10 @@ CONF = cfg.CONF
|
||||
CONF.import_group('haproxy_amphora', 'octavia.common.config')
|
||||
|
||||
|
||||
class HaproxyAmphoraLoadBalancerDriver(driver_base.AmphoraLoadBalancerDriver):
|
||||
class HaproxyAmphoraLoadBalancerDriver(
|
||||
driver_base.AmphoraLoadBalancerDriver,
|
||||
vrrp_rest_driver.KeepalivedAmphoraDriverMixin):
|
||||
|
||||
def __init__(self):
|
||||
super(HaproxyAmphoraLoadBalancerDriver, self).__init__()
|
||||
self.client = AmphoraAPIClient()
|
||||
@@ -61,7 +65,6 @@ class HaproxyAmphoraLoadBalancerDriver(driver_base.AmphoraLoadBalancerDriver):
|
||||
|
||||
# Process listener certificate info
|
||||
certs = self._process_tls_certificates(listener)
|
||||
|
||||
# Generate HaProxy configuration from listener object
|
||||
config = self.jinja.build_config(listener, certs['tls_cert'],
|
||||
certs['sni_certs'])
|
||||
@@ -130,6 +133,9 @@ class HaproxyAmphoraLoadBalancerDriver(driver_base.AmphoraLoadBalancerDriver):
|
||||
port_info = {'mac_address': port.mac_address}
|
||||
self.client.plug_network(amphora, port_info)
|
||||
|
||||
def get_vrrp_interface(self, amphora):
|
||||
return self.client.get_interface(amphora, amphora.vrrp_ip)['interface']
|
||||
|
||||
def _process_tls_certificates(self, listener):
|
||||
"""Processes TLS data from the listener.
|
||||
|
||||
@@ -192,6 +198,10 @@ class AmphoraAPIClient(object):
|
||||
self.stop_listener = functools.partial(self._action, 'stop')
|
||||
self.reload_listener = functools.partial(self._action, 'reload')
|
||||
|
||||
self.start_vrrp = functools.partial(self._vrrp_action, 'start')
|
||||
self.stop_vrrp = functools.partial(self._vrrp_action, 'stop')
|
||||
self.reload_vrrp = functools.partial(self._vrrp_action, 'reload')
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.cert = CONF.haproxy_amphora.client_cert
|
||||
self.ssl_adapter = CustomHostNameCheckingAdapter()
|
||||
@@ -207,7 +217,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
|
||||
|
||||
LOG.debug("request url " + _url)
|
||||
reqargs = {
|
||||
'verify': CONF.haproxy_amphora.server_ca,
|
||||
'url': _url, }
|
||||
@@ -232,7 +242,8 @@ class AmphoraAPIClient(object):
|
||||
def upload_config(self, amp, listener_id, config):
|
||||
r = self.put(
|
||||
amp,
|
||||
'listeners/{listener_id}/haproxy'.format(listener_id=listener_id),
|
||||
'listeners/{amphora_id}/{listener_id}/haproxy'.format(
|
||||
amphora_id=amp.id, listener_id=listener_id),
|
||||
data=config)
|
||||
return exc.check_exception(r)
|
||||
|
||||
@@ -304,3 +315,16 @@ class AmphoraAPIClient(object):
|
||||
'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):
|
||||
r = self.get(amp, 'interface/{ip_addr}'.format(ip_addr=ip_addr))
|
||||
if exc.check_exception(r):
|
||||
return r.json()
|
||||
|
||||
94
octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py
Normal file
94
octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 jinja2
|
||||
from oslo_config import cfg
|
||||
|
||||
from octavia.amphorae.backends.agent.api_server import util
|
||||
from octavia.common import constants
|
||||
|
||||
|
||||
KEEPALIVED_TEMPLATE = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'templates/keepalived_base.template'))
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('keepalived_vrrp', 'octavia.common.config')
|
||||
|
||||
|
||||
class KeepalivedJinjaTemplater(object):
|
||||
|
||||
def __init__(self, keepalived_template=None):
|
||||
"""Keepalived configuration generation
|
||||
|
||||
:param keepalived_template: Absolute path to keepalived Jinja template
|
||||
"""
|
||||
super(KeepalivedJinjaTemplater, self).__init__()
|
||||
self.keepalived_template = (keepalived_template if
|
||||
keepalived_template else
|
||||
KEEPALIVED_TEMPLATE)
|
||||
self._jinja_env = None
|
||||
|
||||
def get_template(self, template_file):
|
||||
"""Returns the specified Jinja configuration template."""
|
||||
if not self._jinja_env:
|
||||
template_loader = jinja2.FileSystemLoader(
|
||||
searchpath=os.path.dirname(template_file))
|
||||
self._jinja_env = jinja2.Environment(
|
||||
loader=template_loader,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True)
|
||||
return self._jinja_env.get_template(os.path.basename(template_file))
|
||||
|
||||
def build_keepalived_config(self, loadbalancer, amphora):
|
||||
"""Renders the loadblanacer keepalived configuration for Active/Standby
|
||||
|
||||
:param loadbalancer: A lodabalancer object
|
||||
:param amp: An amphora object
|
||||
"""
|
||||
# Note on keepalived configuration: The current base configuration
|
||||
# enforced Master election whenever a high priority VRRP instance
|
||||
# start advertising its presence. Accordingly, the fallback behavior
|
||||
# - which I described in the blueprint - is the default behavior.
|
||||
# Although this is a stable behavior, this can be undesirable for
|
||||
# several backend services. To disable the fallback behavior, we need
|
||||
# to add the "nopreempt" flag in the backup instance section.
|
||||
peers_ips = []
|
||||
for amp in loadbalancer.amphorae:
|
||||
if amp.vrrp_ip != amphora.vrrp_ip:
|
||||
peers_ips.append(amp.vrrp_ip)
|
||||
return self.get_template(self.keepalived_template).render(
|
||||
{'vrrp_group_name': loadbalancer.vrrp_group.vrrp_group_name,
|
||||
'amp_role': amphora.role,
|
||||
'amp_intf': amphora.vrrp_interface,
|
||||
'amp_vrrp_id': amphora.vrrp_id,
|
||||
'amp_priority': amphora.vrrp_priority,
|
||||
'vrrp_garp_refresh':
|
||||
CONF.keepalived_vrrp.vrrp_garp_refresh_interval,
|
||||
'vrrp_garp_refresh_repeat':
|
||||
CONF.keepalived_vrrp.vrrp_garp_refresh_count,
|
||||
'vrrp_auth_type': loadbalancer.vrrp_group.vrrp_auth_type,
|
||||
'vrrp_auth_pass': loadbalancer.vrrp_group.vrrp_auth_pass,
|
||||
'amp_vrrp_ip': amphora.vrrp_ip,
|
||||
'peers_vrrp_ips': peers_ips,
|
||||
'vip_ip_address': loadbalancer.vip.ip_address,
|
||||
'advert_int': loadbalancer.vrrp_group.advert_int,
|
||||
'check_script_path': util.keepalived_check_script_path(),
|
||||
'vrrp_check_interval':
|
||||
CONF.keepalived_vrrp.vrrp_check_interval,
|
||||
'vrrp_fail_count': CONF.keepalived_vrrp.vrrp_fail_count,
|
||||
'vrrp_success_count':
|
||||
CONF.keepalived_vrrp.vrrp_success_count},
|
||||
constants=constants)
|
||||
@@ -0,0 +1,55 @@
|
||||
{#
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 unicast_peer_macro(peers_vrrp_ips) %}
|
||||
{% for amp_vrrp_ip in peers_vrrp_ips %}
|
||||
{{ amp_vrrp_ip }}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
vrrp_script check_script {
|
||||
script {{ check_script_path }}
|
||||
interval {{ vrrp_check_interval }}
|
||||
fall {{ vrrp_fail_count }}
|
||||
rise {{ vrrp_success_count }}
|
||||
}
|
||||
|
||||
vrrp_instance {{ vrrp_group_name }} {
|
||||
state {{ amp_role }}
|
||||
interface {{ amp_intf }}
|
||||
virtual_router_id {{ amp_vrrp_id }}
|
||||
priority {{ amp_priority }}
|
||||
garp_master_refresh {{ vrrp_garp_refresh }}
|
||||
garp_master_refresh_repeat {{ vrrp_garp_refresh_repeat }}
|
||||
advert_int {{ advert_int }}
|
||||
authentication {
|
||||
auth_type {{ vrrp_auth_type }}
|
||||
auth_pass {{ vrrp_auth_pass }}
|
||||
}
|
||||
|
||||
unicast_src_ip {{ amp_vrrp_ip }}
|
||||
unicast_peer {
|
||||
{{ unicast_peer_macro(peers_vrrp_ips) }}
|
||||
}
|
||||
|
||||
virtual_ipaddress {
|
||||
{{ vip_ip_address }}
|
||||
}
|
||||
track_script {
|
||||
check_script
|
||||
}
|
||||
}
|
||||
|
||||
79
octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py
Normal file
79
octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from octavia.amphorae.drivers import driver_base as driver_base
|
||||
from octavia.amphorae.drivers.keepalived.jinja import jinja_cfg
|
||||
from octavia.common.config import cfg
|
||||
from octavia.common import constants
|
||||
from octavia.i18n import _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
API_VERSION = constants.API_VERSION
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin):
|
||||
def __init__(self):
|
||||
super(KeepalivedAmphoraDriverMixin, self).__init__()
|
||||
|
||||
# The Mixed class must define a self.client object for the
|
||||
# AmphoraApiClient
|
||||
|
||||
def update_vrrp_conf(self, loadbalancer):
|
||||
"""Update amphorae of the loadbalancer with a new VRRP configuration
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
templater = jinja_cfg.KeepalivedJinjaTemplater()
|
||||
|
||||
LOG.debug("Update loadbalancer %s amphora VRRP configuration.",
|
||||
loadbalancer.id)
|
||||
|
||||
for amp in loadbalancer.amphorae:
|
||||
# Generate Keepalived configuration from loadbalancer object
|
||||
config = templater.build_keepalived_config(loadbalancer, amp)
|
||||
self.client.upload_vrrp_config(amp, config)
|
||||
|
||||
def stop_vrrp_service(self, loadbalancer):
|
||||
"""Stop the vrrp services running on the loadbalancer's amphorae
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
LOG.info(_LI("Stop loadbalancer %s amphora VRRP Service."),
|
||||
loadbalancer.id)
|
||||
for amp in loadbalancer.amphorae:
|
||||
self.client.stop_vrrp(amp)
|
||||
|
||||
def start_vrrp_service(self, loadbalancer):
|
||||
"""Start the VRRP services of all amphorae of the loadbalancer
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
LOG.info(_LI("Start loadbalancer %s amphora VRRP Service."),
|
||||
loadbalancer.id)
|
||||
for amp in loadbalancer.amphorae:
|
||||
LOG.debug("Start VRRP Service on amphora %s .", amp.lb_network_ip)
|
||||
self.client.start_vrrp(amp)
|
||||
|
||||
def reload_vrrp_service(self, loadbalancer):
|
||||
"""Reload the VRRP services of all amphorae of the loadbalancer
|
||||
|
||||
:param loadbalancer: loadbalancer object
|
||||
"""
|
||||
LOG.info(_LI("Reload loadbalancer %s amphora VRRP Service."),
|
||||
loadbalancer.id)
|
||||
for amp in loadbalancer.amphorae:
|
||||
self.client.reload_vrrp(amp)
|
||||
64
octavia/cmd/haproxy_vrrp_check.py
Normal file
64
octavia/cmd/haproxy_vrrp_check.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 socket
|
||||
import sys
|
||||
|
||||
SOCKET_TIMEOUT = 5
|
||||
|
||||
|
||||
def get_status(sock_address):
|
||||
"""Query haproxy stat socket
|
||||
|
||||
Only VRRP fail over if the stats socket is not responding.
|
||||
|
||||
:param sock_address: unix socket file
|
||||
:return: 0 if haproxy responded
|
||||
"""
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.settimeout(SOCKET_TIMEOUT)
|
||||
s.connect(sock_address)
|
||||
s.send('show stat -1 -1 -1\n')
|
||||
data = ''
|
||||
while True:
|
||||
x = s.recv(1024)
|
||||
if not x:
|
||||
break
|
||||
data += x
|
||||
s.close()
|
||||
return 0
|
||||
|
||||
|
||||
def health_check(sock_addresses):
|
||||
"""Invoke queries for all defined listeners
|
||||
|
||||
:param sock_addresses:
|
||||
:return:
|
||||
"""
|
||||
status = 0
|
||||
for address in sock_addresses:
|
||||
status += get_status(address)
|
||||
return status
|
||||
|
||||
|
||||
def main():
|
||||
# usage python haproxy_vrrp_check.py <list_of_stat_sockets>
|
||||
# Note: for performance, this script loads minimal number of module.
|
||||
# Loading octavia modules or any other complex construct MUST be avoided.
|
||||
listeners_sockets = sys.argv[1:]
|
||||
try:
|
||||
status = health_check(listeners_sockets)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
sys.exit(status)
|
||||
@@ -22,7 +22,7 @@ from oslo_db import options as db_options
|
||||
from oslo_log import log as logging
|
||||
import oslo_messaging as messaging
|
||||
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import utils
|
||||
from octavia.i18n import _LI
|
||||
from octavia import version
|
||||
@@ -178,6 +178,9 @@ haproxy_amphora_opts = [
|
||||
cfg.IntOpt('connection_retry_interval',
|
||||
default=5,
|
||||
help=_('Retry timeout between attempts in seconds.')),
|
||||
cfg.StrOpt('haproxy_stick_size', default='10k',
|
||||
help=_('Size of the HAProxy stick table. Accepts k, m, g '
|
||||
'suffixes. Example: 10k')),
|
||||
|
||||
# REST server
|
||||
cfg.IPOpt('bind_host', default='0.0.0.0',
|
||||
@@ -239,7 +242,13 @@ controller_worker_opts = [
|
||||
help=_('Name of the network driver to use')),
|
||||
cfg.StrOpt('cert_generator',
|
||||
default='local_cert_generator',
|
||||
help=_('Name of the cert generator to use'))
|
||||
help=_('Name of the cert generator to use')),
|
||||
cfg.StrOpt('loadbalancer_topology',
|
||||
default=constants.TOPOLOGY_SINGLE,
|
||||
choices=constants.SUPPORTED_LB_TOPOLOGIES,
|
||||
help=_('Load balancer topology configuration. '
|
||||
'SINGLE - One amphora per load balancer. '
|
||||
'ACTIVE_STANDBY - Two amphora per load balancer.'))
|
||||
]
|
||||
|
||||
task_flow_opts = [
|
||||
@@ -301,6 +310,33 @@ anchor_opts = [
|
||||
secret=True)
|
||||
]
|
||||
|
||||
keepalived_vrrp_opts = [
|
||||
cfg.IntOpt('vrrp_advert_int',
|
||||
default=1,
|
||||
help=_('Amphora role and priority advertisement interval '
|
||||
'in seconds.')),
|
||||
cfg.IntOpt('vrrp_check_interval',
|
||||
default=5,
|
||||
help=_('VRRP health check script run interval in seconds.')),
|
||||
cfg.IntOpt('vrrp_fail_count',
|
||||
default=2,
|
||||
help=_('Number of successive failure before transition to a '
|
||||
'fail state.')),
|
||||
cfg.IntOpt('vrrp_success_count',
|
||||
default=2,
|
||||
help=_('Number of successive failure before transition to a '
|
||||
'success state.')),
|
||||
cfg.IntOpt('vrrp_garp_refresh_interval',
|
||||
default=5,
|
||||
help=_('Time in seconds between gratuitous ARP announcements '
|
||||
'from the MASTER.')),
|
||||
cfg.IntOpt('vrrp_garp_refresh_count',
|
||||
default=2,
|
||||
help=_('Number of gratuitous ARP announcements to make on '
|
||||
'each refresh interval.'))
|
||||
|
||||
]
|
||||
|
||||
# Register the configuration options
|
||||
cfg.CONF.register_opts(core_opts)
|
||||
cfg.CONF.register_opts(amphora_agent_opts, group='amphora_agent')
|
||||
@@ -308,6 +344,7 @@ cfg.CONF.register_opts(networking_opts, group='networking')
|
||||
cfg.CONF.register_opts(oslo_messaging_opts, group='oslo_messaging')
|
||||
cfg.CONF.register_opts(haproxy_amphora_opts, group='haproxy_amphora')
|
||||
cfg.CONF.register_opts(controller_worker_opts, group='controller_worker')
|
||||
cfg.CONF.register_opts(keepalived_vrrp_opts, group='keepalived_vrrp')
|
||||
cfg.CONF.register_opts(task_flow_opts, group='task_flow')
|
||||
cfg.CONF.register_opts(oslo_messaging_opts, group='oslo_messaging')
|
||||
cfg.CONF.register_opts(house_keeping_opts, group='house_keeping')
|
||||
|
||||
@@ -102,6 +102,7 @@ AMPHORAE_NETWORK_CONFIG = 'amphorae_network_config'
|
||||
ADDED_PORTS = 'added_ports'
|
||||
PORTS = 'ports'
|
||||
MEMBER_PORTS = 'member_ports'
|
||||
LOADBALANCER_TOPOLOGY = 'topology'
|
||||
|
||||
CERT_ROTATE_AMPHORA_FLOW = 'octavia-cert-rotate-amphora-flow'
|
||||
CREATE_AMPHORA_FLOW = 'octavia-create-amphora-flow'
|
||||
@@ -126,6 +127,42 @@ UPDATE_MEMBER_FLOW = 'octavia-update-member-flow'
|
||||
UPDATE_POOL_FLOW = 'octavia-update-pool-flow'
|
||||
WAIT_FOR_AMPHORA = 'wait-for-amphora'
|
||||
|
||||
FAILOVER_AMPHORA_FLOW = 'octavia-failover-amphora-flow'
|
||||
|
||||
POST_MAP_AMP_TO_LB_SUBFLOW = 'octavia-post-map-amp-to-lb-subflow'
|
||||
CREATE_AMP_FOR_LB_SUBFLOW = 'octavia-create-amp-for-lb-subflow'
|
||||
GET_AMPHORA_FOR_LB_SUBFLOW = 'octavia-get-amphora-for-lb-subflow'
|
||||
POST_LB_AMP_ASSOCIATION_SUBFLOW = (
|
||||
'octavia-post-loadbalancer-amp_association-subflow')
|
||||
|
||||
MAP_LOADBALANCER_TO_AMPHORA = 'octavia-mapload-balancer-to-amphora'
|
||||
RELOAD_AMPHORA = 'octavia-reload-amphora'
|
||||
CREATE_AMPHORA_INDB = 'octavia-create-amphora-indb'
|
||||
GENERATE_SERVER_PEM = 'octavia-generate-serverpem'
|
||||
UPDATE_CERT_EXPIRATION = 'octavia-update-cert-expiration'
|
||||
CERT_COMPUTE_CREATE = 'octavia-cert-compute-create'
|
||||
COMPUTE_CREATE = 'octavia-compute-create'
|
||||
UPDATE_AMPHORA_COMPUTEID = 'octavia-update-amphora-computeid'
|
||||
MARK_AMPHORA_BOOTING_INDB = 'octavia-mark-amphora-booting-indb'
|
||||
WAIT_FOR_AMPHORA = 'octavia-wait_for_amphora'
|
||||
COMPUTE_WAIT = 'octavia-compute-wait'
|
||||
UPDATE_AMPHORA_INFO = 'octavia-update-amphora-info'
|
||||
AMPHORA_FINALIZE = 'octavia-amphora-finalize'
|
||||
MARK_AMPHORA_ALLOCATED_INDB = 'octavia-mark-amphora-allocated-indb'
|
||||
RELOADLOAD_BALANCER = 'octavia-reloadload-balancer'
|
||||
MARK_LB_ACTIVE_INDB = 'octavia-mark-lb-active-indb'
|
||||
MARK_AMP_MASTER_INDB = 'octavia-mark-amp-master-indb'
|
||||
MARK_AMP_BACKUP_INDB = 'octavia-mark-amp-backup-indb'
|
||||
MARK_AMP_STANDALONE_INDB = 'octavia-mark-amp-standalone-indb'
|
||||
GET_VRRP_SUBFLOW = 'octavia-get-vrrp-subflow'
|
||||
AMP_VRRP_UPDATE = 'octavia-amphora-vrrp-update'
|
||||
AMP_VRRP_START = 'octavia-amphora-vrrp-start'
|
||||
AMP_VRRP_STOP = 'octavia-amphora-vrrp-stop'
|
||||
AMP_UPDATE_VRRP_INTF = 'octavia-amphora-update-vrrp-intf'
|
||||
CREATE_VRRP_GROUP_FOR_LB = 'octavia-create-vrrp-group-for-lb'
|
||||
CREATE_VRRP_SECURITY_RULES = 'octavia-create-vrrp-security-rules'
|
||||
|
||||
|
||||
# Task Names
|
||||
RELOAD_LB_AFTER_AMP_ASSOC = 'reload-lb-after-amp-assoc'
|
||||
RELOAD_LB_AFTER_PLUG_VIP = 'reload-lb-after-plug-vip'
|
||||
@@ -137,10 +174,9 @@ NOVA_VERSIONS = (NOVA_1, NOVA_2, NOVA_3)
|
||||
|
||||
RPC_NAMESPACE_CONTROLLER_AGENT = 'controller'
|
||||
|
||||
TOPOLOGY_SINGLE = 'SINGLE'
|
||||
TOPOLOGY_STATUS_OK = 'OK'
|
||||
|
||||
# Active standalone roles and topology
|
||||
TOPOLOGY_SINGLE = 'SINGLE'
|
||||
TOPOLOGY_ACTIVE_STANDBY = 'ACTIVE_STANDBY'
|
||||
ROLE_MASTER = 'MASTER'
|
||||
ROLE_BACKUP = 'BACKUP'
|
||||
@@ -149,6 +185,23 @@ ROLE_STANDALONE = 'STANDALONE'
|
||||
SUPPORTED_LB_TOPOLOGIES = (TOPOLOGY_ACTIVE_STANDBY, TOPOLOGY_SINGLE)
|
||||
SUPPORTED_AMPHORA_ROLES = (ROLE_BACKUP, ROLE_MASTER, ROLE_STANDALONE)
|
||||
|
||||
TOPOLOGY_STATUS_OK = 'OK'
|
||||
|
||||
ROLE_MASTER_PRIORITY = 100
|
||||
ROLE_BACKUP_PRIORITY = 90
|
||||
|
||||
VRRP_AUTH_DEFAULT = 'PASS'
|
||||
VRRP_AUTH_AH = 'AH'
|
||||
SUPPORTED_VRRP_AUTH = (VRRP_AUTH_DEFAULT, VRRP_AUTH_AH)
|
||||
|
||||
KEEPALIVED_CMD = '/usr/local/sbin/keepalived '
|
||||
# The DEFAULT_VRRP_ID value needs to be variable for multi tenant support
|
||||
# per amphora in the future
|
||||
DEFAULT_VRRP_ID = 1
|
||||
VRRP_PROTOCOL_NUM = 112
|
||||
AUTH_HEADER_PROTOCOL_NUMBER = 51
|
||||
|
||||
|
||||
AGENT_API_TEMPLATES = '/templates'
|
||||
AGENT_CONF_TEMPLATE = 'amphora_agent_conf.template'
|
||||
|
||||
@@ -166,6 +219,13 @@ DOWN = 'DOWN'
|
||||
# DOWN = HAProxy backend has no working servers
|
||||
HAPROXY_BACKEND_STATUSES = (UP, DOWN)
|
||||
|
||||
|
||||
NO_CHECK = 'no check'
|
||||
|
||||
HAPROXY_MEMBER_STATUSES = (UP, DOWN, NO_CHECK)
|
||||
|
||||
API_VERSION = '0.5'
|
||||
|
||||
HAPROXY_BASE_PEER_PORT = 1025
|
||||
KEEPALIVED_CONF = 'keepalived.conf.j2'
|
||||
CHECK_SCRIPT_CONF = 'keepalived_check_script.conf.j2'
|
||||
|
||||
@@ -149,7 +149,7 @@ class Listener(BaseDataModel):
|
||||
protocol_port=None, connection_limit=None,
|
||||
enabled=None, provisioning_status=None, operating_status=None,
|
||||
tls_certificate_id=None, stats=None, default_pool=None,
|
||||
load_balancer=None, sni_containers=None):
|
||||
load_balancer=None, sni_containers=None, peer_port=None):
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
@@ -167,13 +167,15 @@ class Listener(BaseDataModel):
|
||||
self.default_pool = default_pool
|
||||
self.load_balancer = load_balancer
|
||||
self.sni_containers = sni_containers
|
||||
self.peer_port = peer_port
|
||||
|
||||
|
||||
class LoadBalancer(BaseDataModel):
|
||||
|
||||
def __init__(self, id=None, tenant_id=None, name=None, description=None,
|
||||
provisioning_status=None, operating_status=None, enabled=None,
|
||||
topology=None, vip=None, listeners=None, amphorae=None):
|
||||
topology=None, vip=None, listeners=None, amphorae=None,
|
||||
vrrp_group=None):
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
@@ -182,11 +184,26 @@ class LoadBalancer(BaseDataModel):
|
||||
self.operating_status = operating_status
|
||||
self.enabled = enabled
|
||||
self.vip = vip
|
||||
self.vrrp_group = vrrp_group
|
||||
self.topology = topology
|
||||
self.listeners = listeners or []
|
||||
self.amphorae = amphorae or []
|
||||
|
||||
|
||||
class VRRPGroup(BaseDataModel):
|
||||
|
||||
def __init__(self, load_balancer_id=None, vrrp_group_name=None,
|
||||
vrrp_auth_type=None, vrrp_auth_pass=None, advert_int=None,
|
||||
smtp_server=None, smtp_connect_timeout=None,
|
||||
load_balancer=None):
|
||||
self.load_balancer_id = load_balancer_id
|
||||
self.vrrp_group_name = vrrp_group_name
|
||||
self.vrrp_auth_type = vrrp_auth_type
|
||||
self.vrrp_auth_pass = vrrp_auth_pass
|
||||
self.advert_int = advert_int
|
||||
self.load_balancer = load_balancer
|
||||
|
||||
|
||||
class Vip(BaseDataModel):
|
||||
|
||||
def __init__(self, load_balancer_id=None, ip_address=None,
|
||||
@@ -226,7 +243,8 @@ class Amphora(BaseDataModel):
|
||||
status=None, lb_network_ip=None, vrrp_ip=None,
|
||||
ha_ip=None, vrrp_port_id=None, ha_port_id=None,
|
||||
load_balancer=None, role=None, cert_expiration=None,
|
||||
cert_busy=False):
|
||||
cert_busy=False, vrrp_interface=None, vrrp_id=None,
|
||||
vrrp_priority=None):
|
||||
self.id = id
|
||||
self.load_balancer_id = load_balancer_id
|
||||
self.compute_id = compute_id
|
||||
@@ -237,6 +255,9 @@ class Amphora(BaseDataModel):
|
||||
self.vrrp_port_id = vrrp_port_id
|
||||
self.ha_port_id = ha_port_id
|
||||
self.role = role
|
||||
self.vrrp_interface = vrrp_interface
|
||||
self.vrrp_id = vrrp_id
|
||||
self.vrrp_priority = vrrp_priority
|
||||
self.load_balancer = load_balancer
|
||||
self.cert_expiration = cert_expiration
|
||||
self.cert_busy = cert_busy
|
||||
|
||||
@@ -175,3 +175,7 @@ class NoSuitableAmphoraException(OctaviaException):
|
||||
# on the instance
|
||||
class ComputeWaitTimeoutException(OctaviaException):
|
||||
message = _LI('Waiting for compute to go active timeout.')
|
||||
|
||||
|
||||
class InvalidTopology(OctaviaException):
|
||||
message = _LE('Invalid topology specified: %(topology)s')
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import random
|
||||
import socket
|
||||
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
|
||||
@@ -50,6 +50,12 @@ def get_random_string(length):
|
||||
return rndstr[0:length]
|
||||
|
||||
|
||||
def base64_sha1_string(string_to_hash):
|
||||
hash_str = hashlib.sha1(string_to_hash.encode('utf-8')).digest()
|
||||
b64_str = base64.b64encode(hash_str, str.encode('_-', 'ascii'))
|
||||
return b64_str.decode('UTF-8')
|
||||
|
||||
|
||||
class exception_logger(object):
|
||||
"""Wrap a function and log raised exception
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import logging
|
||||
|
||||
from octavia.common import base_taskflow
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.controller.worker.flows import amphora_flows
|
||||
from octavia.controller.worker.flows import health_monitor_flows
|
||||
from octavia.controller.worker.flows import listener_flows
|
||||
@@ -28,8 +27,10 @@ from octavia.db import api as db_apis
|
||||
from octavia.db import repositories as repo
|
||||
from octavia.i18n import _LI
|
||||
|
||||
from oslo_config import cfg
|
||||
from taskflow.listeners import logging as tf_logging
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -252,23 +253,37 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
|
||||
# https://review.openstack.org/#/c/98946/
|
||||
|
||||
store = {constants.LOADBALANCER_ID: load_balancer_id}
|
||||
|
||||
topology = CONF.controller_worker.loadbalancer_topology
|
||||
|
||||
if topology == constants.TOPOLOGY_SINGLE:
|
||||
store[constants.UPDATE_DICT] = {constants.LOADBALANCER_TOPOLOGY:
|
||||
constants.TOPOLOGY_SINGLE}
|
||||
elif topology == constants.TOPOLOGY_ACTIVE_STANDBY:
|
||||
store[constants.UPDATE_DICT] = {constants.LOADBALANCER_TOPOLOGY:
|
||||
constants.TOPOLOGY_ACTIVE_STANDBY}
|
||||
# blogan and sbalukoff asked to remove the else check here
|
||||
# as it is also checked later in the flow create code
|
||||
|
||||
create_lb_tf = self._taskflow_load(
|
||||
self._lb_flows.get_create_load_balancer_flow(), store=store)
|
||||
self._lb_flows.get_create_load_balancer_flow(
|
||||
topology=CONF.controller_worker.loadbalancer_topology),
|
||||
store=store)
|
||||
with tf_logging.DynamicLoggingListener(create_lb_tf,
|
||||
log=LOG):
|
||||
try:
|
||||
create_lb_tf.run()
|
||||
except exceptions.NoReadyAmphoraeException:
|
||||
create_amp_lb_tf = self._taskflow_load(
|
||||
self._amphora_flows.get_create_amphora_for_lb_flow(),
|
||||
store=store)
|
||||
|
||||
with tf_logging.DynamicLoggingListener(create_amp_lb_tf,
|
||||
log=LOG):
|
||||
try:
|
||||
create_amp_lb_tf.run()
|
||||
except exceptions.ComputeBuildException as e:
|
||||
raise exceptions.NoSuitableAmphoraException(msg=e.msg)
|
||||
create_lb_tf.run()
|
||||
# Ideally the following flow should be integrated with the
|
||||
# create_active_standby flow. This is not possible with the
|
||||
# current version of taskflow as it flatten out the flows.
|
||||
# Bug report: https://bugs.launchpad.net/taskflow/+bug/1479466
|
||||
post_lb_amp_assoc = self._taskflow_load(
|
||||
self._lb_flows.get_post_lb_amp_association_flow(
|
||||
prefix='post-amphora-association',
|
||||
topology=CONF.controller_worker.loadbalancer_topology),
|
||||
store=store)
|
||||
with tf_logging.DynamicLoggingListener(post_lb_amp_assoc,
|
||||
log=LOG):
|
||||
post_lb_amp_assoc.run()
|
||||
|
||||
def delete_load_balancer(self, load_balancer_id):
|
||||
"""Deletes a load balancer by de-allocating Amphorae.
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
#
|
||||
|
||||
from oslo_config import cfg
|
||||
from taskflow.patterns import graph_flow
|
||||
from taskflow.patterns import linear_flow
|
||||
from taskflow import retry
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.controller.worker.flows import load_balancer_flows
|
||||
from octavia.controller.worker.tasks import amphora_driver_tasks
|
||||
from octavia.controller.worker.tasks import cert_task
|
||||
from octavia.controller.worker.tasks import compute_tasks
|
||||
@@ -34,7 +34,6 @@ class AmphoraFlows(object):
|
||||
# for some reason only this has the values from the config file
|
||||
self.REST_AMPHORA_DRIVER = (CONF.controller_worker.amphora_driver ==
|
||||
'amphora_haproxy_rest_driver')
|
||||
self._lb_flows = load_balancer_flows.LoadBalancerFlows()
|
||||
|
||||
def get_create_amphora_flow(self):
|
||||
"""Creates a flow to create an amphora.
|
||||
@@ -86,68 +85,162 @@ class AmphoraFlows(object):
|
||||
|
||||
return create_amphora_flow
|
||||
|
||||
def get_create_amphora_for_lb_flow(self):
|
||||
"""Creates a flow to create an amphora for a load balancer.
|
||||
def _get_post_map_lb_subflow(self, prefix, role):
|
||||
"""Set amphora type after mapped to lb."""
|
||||
|
||||
This flow is used when there are no spare amphora available
|
||||
for a new load balancer. It builds an amphora and allocates
|
||||
for the specific load balancer.
|
||||
sf_name = prefix + '-' + constants.POST_MAP_AMP_TO_LB_SUBFLOW
|
||||
post_map_amp_to_lb = linear_flow.Flow(
|
||||
sf_name)
|
||||
|
||||
:returns: The The flow for creating the amphora
|
||||
"""
|
||||
create_amp_for_lb_flow = linear_flow.Flow(constants.
|
||||
CREATE_AMPHORA_FOR_LB_FLOW)
|
||||
create_amp_for_lb_flow.add(database_tasks.CreateAmphoraInDB(
|
||||
post_map_amp_to_lb.add(database_tasks.ReloadAmphora(
|
||||
name=sf_name + '-' + constants.RELOAD_AMPHORA,
|
||||
requires=constants.AMPHORA_ID,
|
||||
provides=constants.AMPHORA))
|
||||
|
||||
if role == constants.ROLE_MASTER:
|
||||
post_map_amp_to_lb.add(database_tasks.MarkAmphoraMasterInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_MASTER_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
elif role == constants.ROLE_BACKUP:
|
||||
post_map_amp_to_lb.add(database_tasks.MarkAmphoraBackupInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_BACKUP_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
elif role == constants.ROLE_STANDALONE:
|
||||
post_map_amp_to_lb.add(database_tasks.MarkAmphoraStandAloneInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_STANDALONE_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
|
||||
return post_map_amp_to_lb
|
||||
|
||||
def _get_create_amp_for_lb_subflow(self, prefix, role):
|
||||
"""Create a new amphora for lb."""
|
||||
|
||||
sf_name = prefix + '-' + constants.CREATE_AMP_FOR_LB_SUBFLOW
|
||||
create_amp_for_lb_subflow = linear_flow.Flow(sf_name)
|
||||
|
||||
create_amp_for_lb_subflow.add(database_tasks.CreateAmphoraInDB(
|
||||
name=sf_name + '-' + constants.CREATE_AMPHORA_INDB,
|
||||
provides=constants.AMPHORA_ID))
|
||||
|
||||
if self.REST_AMPHORA_DRIVER:
|
||||
create_amp_for_lb_flow.add(cert_task.GenerateServerPEMTask(
|
||||
create_amp_for_lb_subflow.add(cert_task.GenerateServerPEMTask(
|
||||
name=sf_name + '-' + constants.GENERATE_SERVER_PEM,
|
||||
provides=constants.SERVER_PEM))
|
||||
|
||||
create_amp_for_lb_flow.add(
|
||||
create_amp_for_lb_subflow.add(
|
||||
database_tasks.UpdateAmphoraDBCertExpiration(
|
||||
name=sf_name + '-' + constants.UPDATE_CERT_EXPIRATION,
|
||||
requires=(constants.AMPHORA_ID, constants.SERVER_PEM)))
|
||||
|
||||
create_amp_for_lb_flow.add(compute_tasks.CertComputeCreate(
|
||||
create_amp_for_lb_subflow.add(compute_tasks.CertComputeCreate(
|
||||
name=sf_name + '-' + constants.CERT_COMPUTE_CREATE,
|
||||
requires=(constants.AMPHORA_ID, constants.SERVER_PEM),
|
||||
provides=constants.COMPUTE_ID))
|
||||
else:
|
||||
create_amp_for_lb_flow.add(compute_tasks.ComputeCreate(
|
||||
create_amp_for_lb_subflow.add(compute_tasks.ComputeCreate(
|
||||
name=sf_name + '-' + constants.COMPUTE_CREATE,
|
||||
requires=constants.AMPHORA_ID,
|
||||
provides=constants.COMPUTE_ID))
|
||||
create_amp_for_lb_flow.add(database_tasks.UpdateAmphoraComputeId(
|
||||
|
||||
create_amp_for_lb_subflow.add(database_tasks.UpdateAmphoraComputeId(
|
||||
name=sf_name + '-' + constants.UPDATE_AMPHORA_COMPUTEID,
|
||||
requires=(constants.AMPHORA_ID, constants.COMPUTE_ID)))
|
||||
create_amp_for_lb_flow.add(database_tasks.MarkAmphoraBootingInDB(
|
||||
create_amp_for_lb_subflow.add(database_tasks.MarkAmphoraBootingInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMPHORA_BOOTING_INDB,
|
||||
requires=(constants.AMPHORA_ID, constants.COMPUTE_ID)))
|
||||
wait_flow = linear_flow.Flow(constants.WAIT_FOR_AMPHORA,
|
||||
wait_flow = linear_flow.Flow(sf_name + '-' +
|
||||
constants.WAIT_FOR_AMPHORA,
|
||||
retry=retry.Times(CONF.
|
||||
controller_worker.
|
||||
amp_active_retries))
|
||||
wait_flow.add(compute_tasks.ComputeWait(
|
||||
name=sf_name + '-' + constants.COMPUTE_WAIT,
|
||||
requires=constants.COMPUTE_ID,
|
||||
provides=constants.COMPUTE_OBJ))
|
||||
wait_flow.add(database_tasks.UpdateAmphoraInfo(
|
||||
name=sf_name + '-' + constants.UPDATE_AMPHORA_INFO,
|
||||
requires=(constants.AMPHORA_ID, constants.COMPUTE_OBJ),
|
||||
provides=constants.AMPHORA))
|
||||
create_amp_for_lb_flow.add(wait_flow)
|
||||
create_amp_for_lb_flow.add(amphora_driver_tasks.AmphoraFinalize(
|
||||
create_amp_for_lb_subflow.add(wait_flow)
|
||||
create_amp_for_lb_subflow.add(amphora_driver_tasks.AmphoraFinalize(
|
||||
name=sf_name + '-' + constants.AMPHORA_FINALIZE,
|
||||
requires=constants.AMPHORA))
|
||||
create_amp_for_lb_flow.add(
|
||||
create_amp_for_lb_subflow.add(
|
||||
database_tasks.MarkAmphoraAllocatedInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMPHORA_ALLOCATED_INDB,
|
||||
requires=(constants.AMPHORA, constants.LOADBALANCER_ID)))
|
||||
create_amp_for_lb_flow.add(
|
||||
database_tasks.ReloadAmphora(requires=constants.AMPHORA_ID,
|
||||
provides=constants.AMPHORA))
|
||||
create_amp_for_lb_flow.add(
|
||||
database_tasks.ReloadLoadBalancer(
|
||||
name=constants.RELOAD_LB_AFTER_AMP_ASSOC,
|
||||
requires=constants.LOADBALANCER_ID,
|
||||
provides=constants.LOADBALANCER))
|
||||
new_LB_net_subflow = self._lb_flows.get_new_LB_networking_subflow()
|
||||
create_amp_for_lb_flow.add(new_LB_net_subflow)
|
||||
create_amp_for_lb_flow.add(database_tasks.MarkLBActiveInDB(
|
||||
requires=constants.LOADBALANCER))
|
||||
create_amp_for_lb_subflow.add(database_tasks.ReloadAmphora(
|
||||
name=sf_name + '-' + constants.RELOAD_AMPHORA,
|
||||
requires=constants.AMPHORA_ID,
|
||||
provides=constants.AMPHORA))
|
||||
|
||||
return create_amp_for_lb_flow
|
||||
if role == constants.ROLE_MASTER:
|
||||
create_amp_for_lb_subflow.add(database_tasks.MarkAmphoraMasterInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_MASTER_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
elif role == constants.ROLE_BACKUP:
|
||||
create_amp_for_lb_subflow.add(database_tasks.MarkAmphoraBackupInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_BACKUP_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
elif role == constants.ROLE_STANDALONE:
|
||||
create_amp_for_lb_subflow.add(
|
||||
database_tasks.MarkAmphoraStandAloneInDB(
|
||||
name=sf_name + '-' + constants.MARK_AMP_STANDALONE_INDB,
|
||||
requires=constants.AMPHORA))
|
||||
|
||||
return create_amp_for_lb_subflow
|
||||
|
||||
def _allocate_amp_to_lb_decider(self, history):
|
||||
"""decides if the lb shall be mapped to a spare amphora
|
||||
|
||||
:return: True if a spare amphora exists in DB
|
||||
"""
|
||||
|
||||
return history.values()[0] is not None
|
||||
|
||||
def _create_new_amp_for_lb_decider(self, history):
|
||||
"""decides if a new amphora must be created for the lb
|
||||
|
||||
:return: True
|
||||
"""
|
||||
|
||||
return history.values()[0] is None
|
||||
|
||||
def get_amphora_for_lb_subflow(
|
||||
self, prefix, role=constants.ROLE_STANDALONE):
|
||||
"""Tries to allocate a spare amphora to a loadbalancer if none
|
||||
|
||||
exists, create a new amphora.
|
||||
"""
|
||||
|
||||
sf_name = prefix + '-' + constants.GET_AMPHORA_FOR_LB_SUBFLOW
|
||||
|
||||
# We need a graph flow here for a conditional flow
|
||||
amp_for_lb_flow = graph_flow.Flow(sf_name)
|
||||
|
||||
# Setup the task that maps an amphora to a load balancer
|
||||
allocate_and_associate_amp = database_tasks.MapLoadbalancerToAmphora(
|
||||
name=sf_name + '-' + constants.MAP_LOADBALANCER_TO_AMPHORA,
|
||||
requires=constants.LOADBALANCER_ID,
|
||||
provides=constants.AMPHORA_ID)
|
||||
|
||||
# Define a subflow for if we successfuly map an amphora
|
||||
map_lb_to_amp = self._get_post_map_lb_subflow(prefix, role)
|
||||
# Define a subflow for if we can't map an amphora
|
||||
create_amp = self._get_create_amp_for_lb_subflow(prefix, role)
|
||||
|
||||
# Add them to the graph flow
|
||||
amp_for_lb_flow.add(allocate_and_associate_amp,
|
||||
map_lb_to_amp, create_amp)
|
||||
|
||||
# Setup the decider for the path if we can map an amphora
|
||||
amp_for_lb_flow.link(allocate_and_associate_amp, map_lb_to_amp,
|
||||
decider=self._allocate_amp_to_lb_decider)
|
||||
# Setup the decider for the path if we can't map an amphora
|
||||
amp_for_lb_flow.link(allocate_and_associate_amp, create_amp,
|
||||
decider=self._create_new_amp_for_lb_decider)
|
||||
|
||||
return amp_for_lb_flow
|
||||
|
||||
def get_delete_amphora_flow(self):
|
||||
"""Creates a flow to delete an amphora.
|
||||
|
||||
@@ -30,6 +30,12 @@ class ListenerFlows(object):
|
||||
:returns: The flow for creating a listener
|
||||
"""
|
||||
create_listener_flow = linear_flow.Flow(constants.CREATE_LISTENER_FLOW)
|
||||
create_listener_flow.add(database_tasks.AllocateListenerPeerPort(
|
||||
requires='listener',
|
||||
provides='listener'))
|
||||
create_listener_flow.add(database_tasks.ReloadListener(
|
||||
requires='listener',
|
||||
provides='listener'))
|
||||
create_listener_flow.add(amphora_driver_tasks.ListenerUpdate(
|
||||
requires=[constants.LISTENER, constants.VIP]))
|
||||
create_listener_flow.add(network_tasks.UpdateVIP(
|
||||
@@ -38,7 +44,6 @@ class ListenerFlows(object):
|
||||
MarkLBAndListenerActiveInDB(
|
||||
requires=[constants.LOADBALANCER,
|
||||
constants.LISTENER]))
|
||||
|
||||
return create_listener_flow
|
||||
|
||||
def get_delete_listener_flow(self):
|
||||
|
||||
@@ -14,50 +14,113 @@
|
||||
#
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from taskflow.patterns import linear_flow
|
||||
from taskflow.patterns import unordered_flow
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.controller.worker.flows import amphora_flows
|
||||
from octavia.controller.worker.tasks import amphora_driver_tasks
|
||||
from octavia.controller.worker.tasks import compute_tasks
|
||||
from octavia.controller.worker.tasks import controller_tasks
|
||||
from octavia.controller.worker.tasks import database_tasks
|
||||
from octavia.controller.worker.tasks import network_tasks
|
||||
from octavia.i18n import _LE
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('controller_worker', 'octavia.common.config')
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoadBalancerFlows(object):
|
||||
|
||||
def get_create_load_balancer_flow(self):
|
||||
"""Creates a flow to create a load balancer.
|
||||
def __init__(self):
|
||||
self.amp_flows = amphora_flows.AmphoraFlows()
|
||||
|
||||
:returns: The flow for creating a load balancer
|
||||
def get_create_load_balancer_flow(self, topology):
|
||||
"""Creates a conditional graph flow that allocates a loadbalancer to
|
||||
|
||||
two spare amphorae.
|
||||
:raises InvalidTopology: Invalid topology specified
|
||||
:return: The graph flow for creating an active_standby loadbalancer.
|
||||
"""
|
||||
|
||||
# Note this flow is a bit strange in how it handles building
|
||||
# Amphora if there are no spares. TaskFlow has a spec for
|
||||
# a conditional flow that would make this cleaner once implemented.
|
||||
# https://review.openstack.org/#/c/98946/
|
||||
f_name = constants.CREATE_LOADBALANCER_FLOW
|
||||
lb_create_flow = unordered_flow.Flow(f_name)
|
||||
|
||||
create_LB_flow = linear_flow.Flow(constants.CREATE_LOADBALANCER_FLOW)
|
||||
if topology == constants.TOPOLOGY_ACTIVE_STANDBY:
|
||||
master_amp_sf = self.amp_flows.get_amphora_for_lb_subflow(
|
||||
prefix=constants.ROLE_MASTER, role=constants.ROLE_MASTER)
|
||||
backup_amp_sf = self.amp_flows.get_amphora_for_lb_subflow(
|
||||
prefix=constants.ROLE_BACKUP, role=constants.ROLE_BACKUP)
|
||||
lb_create_flow.add(master_amp_sf, backup_amp_sf)
|
||||
elif topology == constants.TOPOLOGY_SINGLE:
|
||||
amphora_sf = self.amp_flows.get_amphora_for_lb_subflow(
|
||||
prefix=constants.ROLE_STANDALONE,
|
||||
role=constants.ROLE_STANDALONE)
|
||||
lb_create_flow.add(amphora_sf)
|
||||
else:
|
||||
LOG.error(_LE("Unknown topology: %s. Unable to build load "
|
||||
"balancer."), topology)
|
||||
raise exceptions.InvalidTopology(topology=topology)
|
||||
|
||||
return lb_create_flow
|
||||
|
||||
def get_post_lb_amp_association_flow(self, prefix, topology):
|
||||
"""Reload the loadbalancer and create networking subflows for
|
||||
|
||||
created/allocated amphorae.
|
||||
:return: Post amphorae association subflow
|
||||
"""
|
||||
|
||||
# Note: If any task in this flow failed, the created amphorae will be
|
||||
# left ''incorrectly'' allocated to the loadbalancer. Likely,
|
||||
# the get_new_LB_networking_subflow is the most prune to failure
|
||||
# shall deallocate the amphora from its loadbalancer and put it in a
|
||||
# READY state.
|
||||
|
||||
sf_name = prefix + '-' + constants.POST_LB_AMP_ASSOCIATION_SUBFLOW
|
||||
post_create_LB_flow = linear_flow.Flow(sf_name)
|
||||
post_create_LB_flow.add(
|
||||
database_tasks.ReloadLoadBalancer(
|
||||
name=sf_name + '-' + constants.RELOAD_LB_AFTER_AMP_ASSOC,
|
||||
requires=constants.LOADBALANCER_ID,
|
||||
provides=constants.LOADBALANCER))
|
||||
|
||||
create_LB_flow.add(database_tasks.MapLoadbalancerToAmphora(
|
||||
requires=constants.LOADBALANCER_ID,
|
||||
provides=constants.AMPHORA_ID))
|
||||
create_LB_flow.add(database_tasks.ReloadAmphora(
|
||||
requires=constants.AMPHORA_ID,
|
||||
provides=constants.AMPHORA))
|
||||
create_LB_flow.add(database_tasks.ReloadLoadBalancer(
|
||||
name=constants.RELOAD_LB_AFTER_AMP_ASSOC,
|
||||
requires=constants.LOADBALANCER_ID,
|
||||
provides=constants.LOADBALANCER))
|
||||
new_LB_net_subflow = self.get_new_LB_networking_subflow()
|
||||
create_LB_flow.add(new_LB_net_subflow)
|
||||
create_LB_flow.add(database_tasks.MarkLBActiveInDB(
|
||||
requires=constants.LOADBALANCER))
|
||||
post_create_LB_flow.add(new_LB_net_subflow)
|
||||
|
||||
return create_LB_flow
|
||||
if topology == constants.TOPOLOGY_ACTIVE_STANDBY:
|
||||
vrrp_subflow = self._get_vrrp_subflow(prefix)
|
||||
post_create_LB_flow.add(vrrp_subflow)
|
||||
|
||||
post_create_LB_flow.add(database_tasks.UpdateLoadbalancerInDB(
|
||||
requires=[constants.LOADBALANCER, constants.UPDATE_DICT]))
|
||||
post_create_LB_flow.add(database_tasks.MarkLBActiveInDB(
|
||||
name=sf_name + '-' + constants.MARK_LB_ACTIVE_INDB,
|
||||
requires=constants.LOADBALANCER))
|
||||
return post_create_LB_flow
|
||||
|
||||
def _get_vrrp_subflow(self, prefix):
|
||||
sf_name = prefix + '-' + constants.GET_VRRP_SUBFLOW
|
||||
vrrp_subflow = linear_flow.Flow(sf_name)
|
||||
vrrp_subflow.add(amphora_driver_tasks.AmphoraUpdateVRRPInterface(
|
||||
name=sf_name + '-' + constants.AMP_UPDATE_VRRP_INTF,
|
||||
requires=constants.LOADBALANCER,
|
||||
provides=constants.LOADBALANCER))
|
||||
vrrp_subflow.add(database_tasks.CreateVRRPGroupForLB(
|
||||
name=sf_name + '-' + constants.CREATE_VRRP_GROUP_FOR_LB,
|
||||
requires=constants.LOADBALANCER,
|
||||
provides=constants.LOADBALANCER))
|
||||
vrrp_subflow.add(amphora_driver_tasks.AmphoraVRRPUpdate(
|
||||
name=sf_name + '-' + constants.AMP_VRRP_UPDATE,
|
||||
requires=constants.LOADBALANCER))
|
||||
vrrp_subflow.add(amphora_driver_tasks.AmphoraVRRPStart(
|
||||
name=sf_name + '-' + constants.AMP_VRRP_START,
|
||||
requires=constants.LOADBALANCER))
|
||||
return vrrp_subflow
|
||||
|
||||
def get_delete_load_balancer_flow(self):
|
||||
"""Creates a flow to delete a load balancer.
|
||||
|
||||
@@ -50,6 +50,11 @@ class ListenerUpdate(BaseAmphoraTask):
|
||||
|
||||
def execute(self, listener, vip):
|
||||
"""Execute listener update routines for an amphora."""
|
||||
# Ideally this shouldn't be needed. This is a workaround, for a not
|
||||
# very well understood bug not related to Octavia.
|
||||
# https://bugs.launchpad.net/octavia/+bug/1492493
|
||||
listener = self.listener_repo.get(db_apis.get_session(),
|
||||
id=listener.id)
|
||||
self.amphora_driver.update(listener, vip)
|
||||
LOG.debug("Updated amphora with new configuration for listener")
|
||||
|
||||
@@ -238,7 +243,7 @@ class AmphoraPostVIPPlug(BaseAmphoraTask):
|
||||
LOG.warn(_LW("Reverting post vip plug."))
|
||||
self.loadbalancer_repo.update(db_apis.get_session(),
|
||||
id=loadbalancer.id,
|
||||
status=constants.ERROR)
|
||||
provisioning_status=constants.ERROR)
|
||||
|
||||
|
||||
class AmphoraCertUpload(BaseAmphoraTask):
|
||||
@@ -248,3 +253,57 @@ class AmphoraCertUpload(BaseAmphoraTask):
|
||||
"""Execute cert_update_amphora routine."""
|
||||
LOG.debug("Upload cert in amphora REST driver")
|
||||
self.amphora_driver.upload_cert_amp(amphora, server_pem)
|
||||
|
||||
|
||||
class AmphoraUpdateVRRPInterface(BaseAmphoraTask):
|
||||
"""Task to get and update the VRRP interface device name from amphora."""
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
"""Execute post_vip_routine."""
|
||||
amps = []
|
||||
for amp in loadbalancer.amphorae:
|
||||
# Currently this is supported only with REST Driver
|
||||
interface = self.amphora_driver.get_vrrp_interface(amp)
|
||||
self.amphora_repo.update(db_apis.get_session(), amp.id,
|
||||
vrrp_interface=interface)
|
||||
amps.append(self.amphora_repo.get(db_apis.get_session(),
|
||||
id=amp.id))
|
||||
loadbalancer.amphorae = amps
|
||||
return loadbalancer
|
||||
|
||||
def revert(self, result, loadbalancer, *args, **kwargs):
|
||||
"""Handle a failed amphora vip plug notification."""
|
||||
if isinstance(result, failure.Failure):
|
||||
return
|
||||
LOG.warn(_LW("Reverting Get Amphora VRRP Interface."))
|
||||
for amp in loadbalancer.amphorae:
|
||||
self.amphora_repo.update(db_apis.get_session(), amp.id,
|
||||
vrrp_interface=None)
|
||||
|
||||
|
||||
class AmphoraVRRPUpdate(BaseAmphoraTask):
|
||||
"""Task to update the VRRP configuration of the loadbalancer amphorae."""
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
"""Execute update_vrrp_conf."""
|
||||
self.amphora_driver.update_vrrp_conf(loadbalancer)
|
||||
LOG.debug("Uploaded VRRP configuration of loadbalancer %s amphorae",
|
||||
loadbalancer.id)
|
||||
|
||||
|
||||
class AmphoraVRRPStop(BaseAmphoraTask):
|
||||
"""Task to stop keepalived of all amphorae of a LB."""
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
self.amphora_driver.stop_vrrp_service(loadbalancer)
|
||||
LOG.debug("Stopped VRRP of loadbalancer % amphorae",
|
||||
loadbalancer.id)
|
||||
|
||||
|
||||
class AmphoraVRRPStart(BaseAmphoraTask):
|
||||
"""Task to start keepalived of all amphorae of a LB."""
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
self.amphora_driver.start_vrrp_service(loadbalancer)
|
||||
LOG.debug("Started VRRP of loadbalancer %s amphorae",
|
||||
loadbalancer.id)
|
||||
|
||||
@@ -15,20 +15,21 @@
|
||||
|
||||
import logging
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
from taskflow import task
|
||||
from taskflow.types import failure
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import data_models
|
||||
from octavia.common import exceptions
|
||||
import octavia.common.tls_utils.cert_parser as cert_parser
|
||||
from octavia.db import api as db_apis
|
||||
from octavia.db import repositories as repo
|
||||
from octavia.i18n import _LI, _LW
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF.import_group('keepalived_vrrp', 'octavia.common.config')
|
||||
|
||||
|
||||
class BaseDatabaseTask(task.Task):
|
||||
@@ -237,6 +238,14 @@ class ReloadLoadBalancer(BaseDatabaseTask):
|
||||
id=loadbalancer_id)
|
||||
|
||||
|
||||
class ReloadListener(BaseDatabaseTask):
|
||||
"""Reload listener data from database."""
|
||||
|
||||
def execute(self, listener):
|
||||
"""Reloads listener in DB."""
|
||||
return self.listener_repo.get(db_apis.get_session(), id=listener.id)
|
||||
|
||||
|
||||
class UpdateVIPAfterAllocation(BaseDatabaseTask):
|
||||
|
||||
def execute(self, loadbalancer_id, vip):
|
||||
@@ -255,7 +264,8 @@ class UpdateAmphoraVIPData(BaseDatabaseTask):
|
||||
vrrp_ip=amp_data.vrrp_ip,
|
||||
ha_ip=amp_data.ha_ip,
|
||||
vrrp_port_id=amp_data.vrrp_port_id,
|
||||
ha_port_id=amp_data.ha_port_id)
|
||||
ha_port_id=amp_data.ha_port_id,
|
||||
vrrp_id=1)
|
||||
|
||||
|
||||
class AssociateFailoverAmphoraWithLBID(BaseDatabaseTask):
|
||||
@@ -281,24 +291,81 @@ class MapLoadbalancerToAmphora(BaseDatabaseTask):
|
||||
unable to allocate an Amphora
|
||||
"""
|
||||
|
||||
LOG.debug("Allocating an Amphora for load balancer with id %s",
|
||||
LOG.debug("Allocating an Amphora for load balancer with id %s" %
|
||||
loadbalancer_id)
|
||||
|
||||
amp = self.amphora_repo.allocate_and_associate(
|
||||
db_apis.get_session(),
|
||||
loadbalancer_id)
|
||||
if amp is None:
|
||||
LOG.debug("No Amphora available for load balancer with id %s",
|
||||
LOG.debug("No Amphora available for load balancer with id %s" %
|
||||
loadbalancer_id)
|
||||
raise exceptions.NoReadyAmphoraeException()
|
||||
return None
|
||||
|
||||
LOG.info(_LI("Allocated Amphora with id %(amp)s for load balancer "
|
||||
"with id %(lb)s"),
|
||||
{"amp": amp.id, "lb": loadbalancer_id})
|
||||
LOG.debug("Allocated Amphora with id %s for load balancer "
|
||||
"with id %s" % (amp.id, loadbalancer_id))
|
||||
|
||||
return amp.id
|
||||
|
||||
|
||||
class _MarkAmphoraRoleAndPriorityInDB(BaseDatabaseTask):
|
||||
"""Alter the amphora role in DB."""
|
||||
|
||||
def _execute(self, amphora, amp_role, vrrp_priority):
|
||||
LOG.debug("Mark %s in DB for amphora: %s" %
|
||||
(amp_role, amphora.id))
|
||||
self.amphora_repo.update(db_apis.get_session(), amphora.id,
|
||||
role=amp_role,
|
||||
vrrp_priority=vrrp_priority)
|
||||
|
||||
def _revert(self, result, amphora):
|
||||
"""Assigns None role and vrrp_priority."""
|
||||
|
||||
if isinstance(result, failure.Failure):
|
||||
return
|
||||
|
||||
LOG.warn(_LW("Reverting amphora role in DB for amp "
|
||||
"id %(amp)s"),
|
||||
{'amp': amphora.id})
|
||||
self.amphora_repo.update(db_apis.get_session(), amphora.id,
|
||||
role=None,
|
||||
vrrp_priority=None)
|
||||
|
||||
|
||||
class MarkAmphoraMasterInDB(_MarkAmphoraRoleAndPriorityInDB):
|
||||
"""Alter the amphora role to: MASTER."""
|
||||
|
||||
def execute(self, amphora):
|
||||
"""Mark amphora as allocated to a load balancer in DB."""
|
||||
amp_role = constants.ROLE_MASTER
|
||||
self._execute(amphora, amp_role, constants.ROLE_MASTER_PRIORITY)
|
||||
|
||||
def revert(self, result, amphora):
|
||||
self._revert(result, amphora)
|
||||
|
||||
|
||||
class MarkAmphoraBackupInDB(_MarkAmphoraRoleAndPriorityInDB):
|
||||
"""Alter the amphora role to: Backup."""
|
||||
|
||||
def execute(self, amphora):
|
||||
amp_role = constants.ROLE_BACKUP
|
||||
self._execute(amphora, amp_role, constants.ROLE_BACKUP_PRIORITY)
|
||||
|
||||
def revert(self, result, amphora):
|
||||
self._revert(result, amphora)
|
||||
|
||||
|
||||
class MarkAmphoraStandAloneInDB(_MarkAmphoraRoleAndPriorityInDB):
|
||||
"""Alter the amphora role to: Standalone."""
|
||||
|
||||
def execute(self, amphora):
|
||||
amp_role = constants.ROLE_STANDALONE
|
||||
self._execute(amphora, amp_role, None)
|
||||
|
||||
def revert(self, result, amphora):
|
||||
self._revert(result, amphora)
|
||||
|
||||
|
||||
class MarkAmphoraAllocatedInDB(BaseDatabaseTask):
|
||||
"""Will mark an amphora as allocated to a load balancer in the database.
|
||||
|
||||
@@ -685,7 +752,7 @@ class MarkListenerPendingDeleteInDB(BaseDatabaseTask):
|
||||
|
||||
|
||||
class UpdateLoadbalancerInDB(BaseDatabaseTask):
|
||||
"""Update the listener in the DB.
|
||||
"""Update the loadbalancer in the DB.
|
||||
|
||||
Since sqlalchemy will likely retry by itself always revert if it fails
|
||||
"""
|
||||
@@ -698,7 +765,8 @@ class UpdateLoadbalancerInDB(BaseDatabaseTask):
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
LOG.debug("Update DB for listener id: %s ", loadbalancer.id)
|
||||
LOG.debug("Update DB for loadbalancer id: %s " %
|
||||
loadbalancer.id)
|
||||
self.loadbalancer_repo.update(db_apis.get_session(), loadbalancer.id,
|
||||
**update_dict)
|
||||
|
||||
@@ -710,7 +778,7 @@ class UpdateLoadbalancerInDB(BaseDatabaseTask):
|
||||
|
||||
LOG.warn(_LW("Reverting update listener in DB "
|
||||
"for listener id %s"), listener.id)
|
||||
# TODO(johnsom) fix this to set the upper ojects to ERROR
|
||||
# TODO(johnsom) fix this to set the upper objects to ERROR
|
||||
self.listener_repo.update(db_apis.get_session(), listener.id,
|
||||
enabled=0)
|
||||
|
||||
@@ -868,3 +936,49 @@ class GetVipFromLoadbalancer(BaseDatabaseTask):
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
return loadbalancer.vip
|
||||
|
||||
|
||||
class CreateVRRPGroupForLB(BaseDatabaseTask):
|
||||
|
||||
def execute(self, loadbalancer):
|
||||
loadbalancer.vrrp_group = self.repos.vrrpgroup.create(
|
||||
db_apis.get_session(),
|
||||
load_balancer_id=loadbalancer.id,
|
||||
vrrp_group_name=str(loadbalancer.id).replace('-', ''),
|
||||
vrrp_auth_type=constants.VRRP_AUTH_DEFAULT,
|
||||
vrrp_auth_pass=uuidutils.generate_uuid().replace('-', '')[0:7],
|
||||
advert_int=CONF.keepalived_vrrp.vrrp_advert_int)
|
||||
return loadbalancer
|
||||
|
||||
|
||||
class AllocateListenerPeerPort(BaseDatabaseTask):
|
||||
"""Get a new peer port number for a listener."""
|
||||
|
||||
def execute(self, listener):
|
||||
"""Allocate a peer port (TCP port)
|
||||
|
||||
:param listener: The listener to be marked deleted
|
||||
:returns: None
|
||||
"""
|
||||
max_peer_port = 0
|
||||
for listener in listener.load_balancer.listeners:
|
||||
if listener.peer_port > max_peer_port:
|
||||
max_peer_port = listener.peer_port
|
||||
|
||||
if max_peer_port == 0:
|
||||
port = constants.HAPROXY_BASE_PEER_PORT
|
||||
else:
|
||||
port = max_peer_port + 1
|
||||
|
||||
self.listener_repo.update(db_apis.get_session(), listener.id,
|
||||
peer_port=port)
|
||||
|
||||
return self.listener_repo.get(db_apis.get_session(), id=listener.id)
|
||||
|
||||
def revert(self, listener, *args, **kwargs):
|
||||
"""nulls the peer port
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
self.listener_repo.update(db_apis.get_session(), listener.id,
|
||||
peer_port=None)
|
||||
|
||||
@@ -224,7 +224,6 @@ class HandleNetworkDeltas(BaseNetworkTask):
|
||||
|
||||
def execute(self, deltas):
|
||||
"""Handle network plugging based off deltas."""
|
||||
|
||||
added_ports = {}
|
||||
for amp_id, delta in six.iteritems(deltas):
|
||||
added_ports[amp_id] = []
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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.
|
||||
"""Keepalived configuration datamodel
|
||||
|
||||
Revision ID: 1e4c1d83044c
|
||||
Revises: 5a3ee5472c31
|
||||
Create Date: 2015-08-06 10:39:54.998797
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1e4c1d83044c'
|
||||
down_revision = '5a3ee5472c31'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import sql
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
u'vrrp_auth_method',
|
||||
sa.Column(u'name', sa.String(36), primary_key=True),
|
||||
sa.Column(u'description', sa.String(255), nullable=True)
|
||||
)
|
||||
|
||||
insert_table = sql.table(
|
||||
u'vrrp_auth_method',
|
||||
sql.column(u'name', sa.String),
|
||||
sql.column(u'description', sa.String)
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
insert_table,
|
||||
[
|
||||
{'name': 'PASS'},
|
||||
{'name': 'AH'}
|
||||
]
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
u'vrrp_group',
|
||||
sa.Column(u'load_balancer_id', sa.String(36), nullable=False),
|
||||
sa.Column(u'vrrp_group_name', sa.String(36), nullable=True),
|
||||
sa.Column(u'vrrp_auth_type', sa.String(16), nullable=True),
|
||||
sa.Column(u'vrrp_auth_pass', sa.String(36), nullable=True),
|
||||
sa.Column(u'advert_int', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint(u'load_balancer_id'),
|
||||
sa.ForeignKeyConstraint([u'load_balancer_id'], [u'load_balancer.id'],
|
||||
name=u'fk_vrrp_group_load_balancer_id'),
|
||||
sa.ForeignKeyConstraint([u'vrrp_auth_type'],
|
||||
[u'vrrp_auth_method.name'],
|
||||
name=u'fk_load_balancer_vrrp_auth_method_name')
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
u'listener',
|
||||
sa.Column(u'peer_port', sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
u'amphora',
|
||||
sa.Column(u'vrrp_interface', sa.String(16), nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
u'amphora',
|
||||
sa.Column(u'vrrp_id', sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
u'amphora',
|
||||
sa.Column(u'vrrp_priority', sa.Integer(), nullable=True)
|
||||
)
|
||||
@@ -62,6 +62,11 @@ class HealthMonitorType(base_models.BASE, base_models.LookupTableMixin):
|
||||
__tablename__ = "health_monitor_type"
|
||||
|
||||
|
||||
class VRRPAuthMethod(base_models.BASE, base_models.LookupTableMixin):
|
||||
|
||||
__tablename__ = "vrrp_auth_method"
|
||||
|
||||
|
||||
class SessionPersistence(base_models.BASE):
|
||||
|
||||
__data_model__ = data_models.SessionPersistence
|
||||
@@ -232,6 +237,30 @@ class LoadBalancer(base_models.BASE, base_models.IdMixin,
|
||||
uselist=False))
|
||||
|
||||
|
||||
class VRRPGroup(base_models.BASE):
|
||||
|
||||
__data_model__ = data_models.VRRPGroup
|
||||
|
||||
__tablename__ = "vrrp_group"
|
||||
|
||||
load_balancer_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey("load_balancer.id",
|
||||
name="fk_vrrp_group_load_balancer_id"),
|
||||
nullable=False, primary_key=True)
|
||||
|
||||
vrrp_group_name = sa.Column(sa.String(36), nullable=True)
|
||||
vrrp_auth_type = sa.Column(sa.String(16), sa.ForeignKey(
|
||||
"vrrp_auth_method.name",
|
||||
name="fk_load_balancer_vrrp_auth_method_name"))
|
||||
vrrp_auth_pass = sa.Column(sa.String(36), nullable=True)
|
||||
advert_int = sa.Column(sa.Integer(), nullable=True)
|
||||
load_balancer = orm.relationship("LoadBalancer", uselist=False,
|
||||
backref=orm.backref("vrrp_group",
|
||||
uselist=False,
|
||||
cascade="delete"))
|
||||
|
||||
|
||||
class Vip(base_models.BASE):
|
||||
|
||||
__data_model__ = data_models.Vip
|
||||
@@ -299,6 +328,7 @@ class Listener(base_models.BASE, base_models.IdMixin, base_models.TenantMixin):
|
||||
backref=orm.backref("listener",
|
||||
uselist=False),
|
||||
cascade="delete")
|
||||
peer_port = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
|
||||
class SNI(base_models.BASE):
|
||||
@@ -352,6 +382,9 @@ class Amphora(base_models.BASE):
|
||||
sa.String(36),
|
||||
sa.ForeignKey("provisioning_status.name",
|
||||
name="fk_container_provisioning_status_name"))
|
||||
vrrp_interface = sa.Column(sa.String(16), nullable=True)
|
||||
vrrp_id = sa.Column(sa.Integer(), nullable=True)
|
||||
vrrp_priority = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
|
||||
class AmphoraHealth(base_models.BASE):
|
||||
|
||||
@@ -132,6 +132,7 @@ class Repositories(object):
|
||||
self.amphora = AmphoraRepository()
|
||||
self.sni = SNIRepository()
|
||||
self.amphorahealth = AmphoraHealthRepository()
|
||||
self.vrrpgroup = VRRPGroupRepository()
|
||||
|
||||
def create_load_balancer_and_vip(self, session, lb_dict, vip_dict):
|
||||
"""Inserts load balancer and vip entities into the database.
|
||||
@@ -507,3 +508,13 @@ class AmphoraHealthRepository(BaseRepository):
|
||||
amp.busy = True
|
||||
|
||||
return amp.to_data_model()
|
||||
|
||||
|
||||
class VRRPGroupRepository(BaseRepository):
|
||||
model_class = models.VRRPGroup
|
||||
|
||||
def update(self, session, load_balancer_id, **model_kwargs):
|
||||
"""Updates a VRRPGroup entry for by load_balancer_id."""
|
||||
with session.begin(subtransactions=True):
|
||||
session.query(self.model_class).filter_by(
|
||||
load_balancer_id=load_balancer_id).update(model_kwargs)
|
||||
|
||||
@@ -103,7 +103,13 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
|
||||
security_group_id=sec_grp_id)
|
||||
updated_ports = [
|
||||
listener.protocol_port for listener in load_balancer.listeners
|
||||
if listener.provisioning_status != constants.PENDING_DELETE]
|
||||
if listener.provisioning_status != constants.PENDING_DELETE and
|
||||
listener.provisioning_status != constants.DELETED]
|
||||
peer_ports = [
|
||||
listener.peer_port for listener in load_balancer.listeners
|
||||
if listener.provisioning_status != constants.PENDING_DELETE and
|
||||
listener.provisioning_status != constants.DELETED]
|
||||
updated_ports.extend(peer_ports)
|
||||
# Just going to use port_range_max for now because we can assume that
|
||||
# port_range_max and min will be the same since this driver is
|
||||
# responsible for creating these rules
|
||||
@@ -120,6 +126,31 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
|
||||
self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port,
|
||||
port_max=port)
|
||||
|
||||
# Currently we are using the VIP network for VRRP
|
||||
# so we need to open up the protocols for it
|
||||
if (cfg.CONF.controller_worker.loadbalancer_topology ==
|
||||
constants.TOPOLOGY_ACTIVE_STANDBY):
|
||||
try:
|
||||
self._create_security_group_rule(
|
||||
sec_grp_id,
|
||||
constants.VRRP_PROTOCOL_NUM,
|
||||
direction='ingress')
|
||||
except neutron_client_exceptions.Conflict:
|
||||
# It's ok if this rule already exists
|
||||
pass
|
||||
except Exception as e:
|
||||
raise base.PlugVIPException(str(e))
|
||||
|
||||
try:
|
||||
self._create_security_group_rule(
|
||||
sec_grp_id, constants.AUTH_HEADER_PROTOCOL_NUMBER,
|
||||
direction='ingress')
|
||||
except neutron_client_exceptions.Conflict:
|
||||
# It's ok if this rule already exists
|
||||
pass
|
||||
except Exception as e:
|
||||
raise base.PlugVIPException(str(e))
|
||||
|
||||
def _update_vip_security_group(self, load_balancer, vip):
|
||||
sec_grp = self._get_lb_security_group(load_balancer.id)
|
||||
if not sec_grp:
|
||||
|
||||
@@ -30,4 +30,5 @@ def list_opts():
|
||||
('task_flow', octavia.common.config.task_flow_opts),
|
||||
('certificates', octavia.common.config.certificate_opts),
|
||||
('house_keeping', octavia.common.config.house_keeping_opts)
|
||||
('keepalived_vrrp', octavia.common.config.keepalived_vrrp_opts)
|
||||
]
|
||||
|
||||
@@ -25,6 +25,7 @@ from octavia.amphorae.backends.agent.api_server import certificate_update
|
||||
from octavia.amphorae.backends.agent.api_server import server
|
||||
from octavia.amphorae.backends.agent.api_server import util
|
||||
from octavia.common import constants as consts
|
||||
from octavia.common import utils as octavia_utils
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
RANDOM_ERROR = 'random error'
|
||||
@@ -56,14 +57,18 @@ class ServerTestCase(base.TestCase):
|
||||
# happy case upstart file exists
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION +
|
||||
'/listeners/123/haproxy', data='test')
|
||||
'/listeners/amp_123/123/haproxy',
|
||||
data='test')
|
||||
self.assertEqual(202, rv.status_code)
|
||||
m.assert_called_once_with(
|
||||
'/var/lib/octavia/123/haproxy.cfg.new', 'w')
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(six.b('test'))
|
||||
mock_subprocess.assert_called_once_with(
|
||||
"haproxy -c -f /var/lib/octavia/123/haproxy.cfg.new".split(),
|
||||
"haproxy -c -L {peer} -f {config_file}".format(
|
||||
config_file='/var/lib/octavia/123/haproxy.cfg.new',
|
||||
peer=(octavia_utils.
|
||||
base64_sha1_string('amp_123').rstrip('='))).split(),
|
||||
stderr=-2)
|
||||
mock_rename.assert_called_once_with(
|
||||
'/var/lib/octavia/123/haproxy.cfg.new',
|
||||
@@ -74,7 +79,8 @@ class ServerTestCase(base.TestCase):
|
||||
m.side_effect = IOError() # open crashes
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION +
|
||||
'/listeners/123/haproxy', data='test')
|
||||
'/listeners/amp_123/123/haproxy',
|
||||
data='test')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
|
||||
# check if files get created
|
||||
@@ -84,7 +90,8 @@ class ServerTestCase(base.TestCase):
|
||||
# happy case upstart file exists
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION +
|
||||
'/listeners/123/haproxy', data='test')
|
||||
'/listeners/amp_123/123/haproxy',
|
||||
data='test')
|
||||
self.assertEqual(202, rv.status_code)
|
||||
m.assert_any_call('/var/lib/octavia/123/haproxy.cfg.new', 'w')
|
||||
m.assert_any_call(util.UPSTART_DIR + '/haproxy-123.conf', 'w')
|
||||
@@ -99,7 +106,8 @@ class ServerTestCase(base.TestCase):
|
||||
7, 'test', RANDOM_ERROR)]
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION +
|
||||
'/listeners/123/haproxy', data='test')
|
||||
'/listeners/amp_123/123/haproxy',
|
||||
data='test')
|
||||
self.assertEqual(400, rv.status_code)
|
||||
self.assertEqual(
|
||||
{'message': 'Invalid request', u'details': u'random error'},
|
||||
@@ -108,14 +116,19 @@ class ServerTestCase(base.TestCase):
|
||||
handle = m()
|
||||
handle.write.assert_called_with(six.b('test'))
|
||||
mock_subprocess.assert_called_with(
|
||||
"haproxy -c -f /var/lib/octavia/123/haproxy.cfg.new".split(),
|
||||
"haproxy -c -L {peer} -f {config_file}".format(
|
||||
config_file='/var/lib/octavia/123/haproxy.cfg.new',
|
||||
peer=(octavia_utils.
|
||||
base64_sha1_string('amp_123').rstrip('='))).split(),
|
||||
stderr=-2)
|
||||
mock_remove.assert_called_once_with(
|
||||
'/var/lib/octavia/123/haproxy.cfg.new')
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
|
||||
'vrrp_check_script_update')
|
||||
@mock.patch('subprocess.check_output')
|
||||
def test_start(self, mock_subprocess, mock_exists):
|
||||
def test_start(self, mock_subprocess, mock_vrrp, mock_exists):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/listeners/123/error')
|
||||
self.assertEqual(400, rv.status_code)
|
||||
self.assertEqual(
|
||||
@@ -609,3 +622,86 @@ class ServerTestCase(base.TestCase):
|
||||
{'details': RANDOM_ERROR,
|
||||
'message': 'Error plugging VIP'},
|
||||
json.loads(rv.data.decode('utf-8')))
|
||||
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('netifaces.interfaces')
|
||||
def test_get_interface(self, mock_interfaces, mock_ifaddresses):
|
||||
interface_res = {'interface': 'eth0'}
|
||||
mock_interfaces.return_value = ['lo', 'eth0']
|
||||
mock_ifaddresses.return_value = {
|
||||
17: [{'addr': '00:00:00:00:00:00'}],
|
||||
2: [{'addr': '203.0.113.2'}],
|
||||
10: [{'addr': '::1'}]}
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/203.0.113.2',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/::1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/10.0.0.1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(404, rv.status_code)
|
||||
|
||||
rv = self.app.get('/' + api_server.VERSION +
|
||||
'/interface/00:00:00:00:00:00',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.makedirs')
|
||||
@mock.patch('os.rename')
|
||||
@mock.patch('subprocess.check_output')
|
||||
@mock.patch('os.remove')
|
||||
def test_upload_keepalived_config(self, mock_remove, mock_subprocess,
|
||||
mock_rename, mock_makedirs, mock_exists):
|
||||
|
||||
mock_exists.return_value = True
|
||||
m = mock.mock_open()
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
|
||||
data='test')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
mock_exists.return_value = False
|
||||
m = mock.mock_open()
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
|
||||
data='test')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
mock_subprocess.side_effect = subprocess.CalledProcessError(1,
|
||||
'blah!')
|
||||
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
|
||||
data='test')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
|
||||
mock_subprocess.side_effect = [True,
|
||||
subprocess.CalledProcessError(1,
|
||||
'blah!')]
|
||||
|
||||
with mock.patch.object(builtins, 'open', m):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
|
||||
data='test')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
|
||||
@mock.patch('subprocess.check_output')
|
||||
def test_manage_service_vrrp(self, mock_check_output):
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/start')
|
||||
self.assertEqual(202, rv.status_code)
|
||||
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/restart')
|
||||
self.assertEqual(400, rv.status_code)
|
||||
|
||||
mock_check_output.side_effect = subprocess.CalledProcessError(1,
|
||||
'blah!')
|
||||
|
||||
rv = self.app.put('/' + api_server.VERSION + '/vrrp/start')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
|
||||
@@ -69,6 +69,8 @@ class OctaviaDBTestBase(test_base.DbTestCase):
|
||||
models.AmphoraRoles)
|
||||
self._seed_lookup_table(session, constants.SUPPORTED_LB_TOPOLOGIES,
|
||||
models.LBTopology)
|
||||
self._seed_lookup_table(session, constants.SUPPORTED_VRRP_AUTH,
|
||||
models.VRRPAuthMethod)
|
||||
|
||||
def _seed_lookup_table(self, session, name_list, model_cls):
|
||||
for name in name_list:
|
||||
|
||||
@@ -50,6 +50,7 @@ class BaseRepositoryTest(base.OctaviaDBTestBase):
|
||||
self.sni_repo = repo.SNIRepository()
|
||||
self.amphora_repo = repo.AmphoraRepository()
|
||||
self.amphora_health_repo = repo.AmphoraHealthRepository()
|
||||
self.vrrp_group_repo = repo.VRRPGroupRepository()
|
||||
|
||||
def test_get_all_return_value(self):
|
||||
pool_list = self.pool_repo.get_all(self.session,
|
||||
@@ -80,7 +81,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
|
||||
repo_attr_names = ('load_balancer', 'vip', 'health_monitor',
|
||||
'session_persistence', 'pool', 'member', 'listener',
|
||||
'listener_stats', 'amphora', 'sni',
|
||||
'amphorahealth')
|
||||
'amphorahealth', 'vrrpgroup')
|
||||
for repo_attr in repo_attr_names:
|
||||
single_repo = getattr(self.repos, repo_attr, None)
|
||||
message = ("Class Repositories should have %s instance"
|
||||
@@ -106,7 +107,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
|
||||
lb = {'name': 'test1', 'description': 'desc1', 'enabled': True,
|
||||
'provisioning_status': constants.PENDING_UPDATE,
|
||||
'operating_status': constants.OFFLINE,
|
||||
'topology': constants.TOPOLOGY_ACTIVE_STANDBY}
|
||||
'topology': constants.TOPOLOGY_ACTIVE_STANDBY,
|
||||
'vrrp_group': None}
|
||||
vip = {'ip_address': '10.0.0.1',
|
||||
'port_id': uuidutils.generate_uuid(),
|
||||
'subnet_id': uuidutils.generate_uuid()}
|
||||
@@ -1346,3 +1348,48 @@ class AmphoraHealthRepositoryTest(BaseRepositoryTest):
|
||||
self.session, amphora_id=amphora_health.amphora_id)
|
||||
self.assertIsNone(self.amphora_health_repo.get(
|
||||
self.session, amphora_id=amphora_health.amphora_id))
|
||||
|
||||
|
||||
class VRRPGroupRepositoryTest(BaseRepositoryTest):
|
||||
def setUp(self):
|
||||
super(VRRPGroupRepositoryTest, self).setUp()
|
||||
self.lb = self.lb_repo.create(
|
||||
self.session, id=self.FAKE_UUID_1, tenant_id=self.FAKE_UUID_2,
|
||||
name="lb_name", description="lb_description",
|
||||
provisioning_status=constants.ACTIVE,
|
||||
operating_status=constants.ONLINE, enabled=True)
|
||||
|
||||
def test_update(self):
|
||||
self.vrrpgroup = self.vrrp_group_repo.create(
|
||||
self.session,
|
||||
load_balancer_id=self.lb.id,
|
||||
vrrp_group_name='TESTVRRPGROUP',
|
||||
vrrp_auth_type=constants.VRRP_AUTH_DEFAULT,
|
||||
vrrp_auth_pass='TESTPASS',
|
||||
advert_int=1)
|
||||
|
||||
# Validate baseline
|
||||
old_vrrp_group = self.vrrp_group_repo.get(self.session,
|
||||
load_balancer_id=self.lb.id)
|
||||
|
||||
self.assertEqual('TESTVRRPGROUP', old_vrrp_group.vrrp_group_name)
|
||||
self.assertEqual(constants.VRRP_AUTH_DEFAULT,
|
||||
old_vrrp_group.vrrp_auth_type)
|
||||
self.assertEqual('TESTPASS', old_vrrp_group.vrrp_auth_pass)
|
||||
self.assertEqual(1, old_vrrp_group.advert_int)
|
||||
|
||||
# Test update
|
||||
self.vrrp_group_repo.update(self.session,
|
||||
load_balancer_id=self.lb.id,
|
||||
vrrp_group_name='TESTVRRPGROUP2',
|
||||
vrrp_auth_type='AH',
|
||||
vrrp_auth_pass='TESTPASS2',
|
||||
advert_int=2)
|
||||
|
||||
new_vrrp_group = self.vrrp_group_repo.get(self.session,
|
||||
load_balancer_id=self.lb.id)
|
||||
|
||||
self.assertEqual('TESTVRRPGROUP2', new_vrrp_group.vrrp_group_name)
|
||||
self.assertEqual('AH', new_vrrp_group.vrrp_auth_type)
|
||||
self.assertEqual('TESTPASS2', new_vrrp_group.vrrp_auth_pass)
|
||||
self.assertEqual(2, new_vrrp_group.advert_int)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 flask
|
||||
import mock
|
||||
|
||||
import subprocess
|
||||
|
||||
from octavia.amphorae.backends.agent.api_server import keepalived
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
|
||||
class KeepalivedTestCase(base.TestCase):
|
||||
def setUp(self):
|
||||
super(KeepalivedTestCase, self).setUp()
|
||||
self.app = flask.Flask(__name__)
|
||||
self.client = self.app.test_client()
|
||||
self._ctx = self.app.test_request_context()
|
||||
self._ctx.push()
|
||||
|
||||
@mock.patch('subprocess.check_output')
|
||||
def test_manager_keepalived_service(self, mock_check_output):
|
||||
res = keepalived.manager_keepalived_service('start')
|
||||
cmd = ("/usr/sbin/service octavia-keepalived {action}".format(
|
||||
action='start'))
|
||||
mock_check_output.assert_called_once_with(cmd.split(),
|
||||
stderr=subprocess.STDOUT)
|
||||
self.assertEqual(202, res.status_code)
|
||||
|
||||
res = keepalived.manager_keepalived_service('restart')
|
||||
self.assertEqual(400, res.status_code)
|
||||
|
||||
mock_check_output.side_effect = subprocess.CalledProcessError(1,
|
||||
'blah!')
|
||||
|
||||
res = keepalived.manager_keepalived_service('start')
|
||||
self.assertEqual(500, res.status_code)
|
||||
@@ -130,3 +130,34 @@ class ListenerTestCase(base.TestCase):
|
||||
self.assertEqual(
|
||||
consts.OFFLINE,
|
||||
listener._check_listener_status('123'))
|
||||
|
||||
@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')
|
||||
@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,
|
||||
mock_join, mock_listdir, mock_exists,
|
||||
mock_makedirs):
|
||||
mock_get_listeners.return_value = ['abc', '123']
|
||||
mock_sock_path.return_value = 'listener.sock'
|
||||
mock_exists.return_value = False
|
||||
cmd = 'haproxy-vrrp-check ' + ' '.join(['listener.sock']) + '; exit $?'
|
||||
m = mock.mock_open()
|
||||
with mock.patch('%s.open' % BUILTINS, m, create=True):
|
||||
listener.vrrp_check_script_update('123', 'stop')
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(cmd)
|
||||
|
||||
mock_get_listeners.return_value = ['abc', '123']
|
||||
cmd = ('haproxy-vrrp-check ' + ' '.join(['listener.sock',
|
||||
'listener.sock']) + '; exit '
|
||||
'$?')
|
||||
m = mock.mock_open()
|
||||
with mock.patch('%s.open' % BUILTINS, m, create=True):
|
||||
listener.vrrp_check_script_update('123', 'start')
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(cmd)
|
||||
|
||||
@@ -33,6 +33,7 @@ FAKE_SUBNET_INFO = {'subnet_cidr': FAKE_CIDR,
|
||||
'gateway': FAKE_GATEWAY,
|
||||
'mac_address': '123'}
|
||||
FAKE_UUID_1 = uuidutils.generate_uuid()
|
||||
FAKE_VRRP_IP = '10.1.0.1'
|
||||
|
||||
|
||||
class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
@@ -157,6 +158,11 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
self.driver.client.plug_network.assert_called_once_with(
|
||||
self.amp, dict(mac_address='123'))
|
||||
|
||||
def test_get_vrrp_interface(self):
|
||||
self.driver.get_vrrp_interface(self.amp)
|
||||
self.driver.client.get_interface.assert_called_once_with(
|
||||
self.amp, self.amp.vrrp_ip)
|
||||
|
||||
|
||||
class TestAmphoraAPIClientTest(base.TestCase):
|
||||
|
||||
@@ -453,7 +459,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
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,
|
||||
filename=FAKE_PEM_FILENAME), status_code=403)
|
||||
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")
|
||||
@@ -494,7 +500,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_update_invalid_cert_for_rotation(self, m):
|
||||
m.put("{base}/certificate".format(base=self.base_url), status_code=403)
|
||||
m.put("{base}/certificate".format(base=self.base_url), status_code=400)
|
||||
self.assertRaises(exc.InvalidRequest,
|
||||
self.driver.update_cert_for_rotation, self.amp,
|
||||
"some_file")
|
||||
@@ -612,19 +618,24 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
def test_upload_config(self, m):
|
||||
config = {"name": "fake_config"}
|
||||
m.put(
|
||||
"{base}/listeners/{listener_id}/haproxy".format(
|
||||
base=self.base_url, listener_id=FAKE_UUID_1),
|
||||
"{base}/listeners/{"
|
||||
"amphora_id}/{listener_id}/haproxy".format(
|
||||
amphora_id=self.amp.id, base=self.base_url,
|
||||
listener_id=FAKE_UUID_1),
|
||||
json=config)
|
||||
self.driver.upload_config(self.amp, FAKE_UUID_1, 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}/listeners/{listener_id}/haproxy".format(
|
||||
base=self.base_url, listener_id=FAKE_UUID_1),
|
||||
status_code=403)
|
||||
"{base}/listeners/{"
|
||||
"amphora_id}/{listener_id}/haproxy".format(
|
||||
amphora_id=self.amp.id, base=self.base_url,
|
||||
listener_id=FAKE_UUID_1),
|
||||
status_code=400)
|
||||
self.assertRaises(exc.InvalidRequest, self.driver.upload_config,
|
||||
self.amp, FAKE_UUID_1, config)
|
||||
|
||||
@@ -632,8 +643,10 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
def test_upload_config_unauthorized(self, m):
|
||||
config = '{"name": "bad_config"}'
|
||||
m.put(
|
||||
"{base}/listeners/{listener_id}/haproxy".format(
|
||||
base=self.base_url, listener_id=FAKE_UUID_1),
|
||||
"{base}/listeners/{"
|
||||
"amphora_id}/{listener_id}/haproxy".format(
|
||||
amphora_id=self.amp.id, base=self.base_url,
|
||||
listener_id=FAKE_UUID_1),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.upload_config,
|
||||
self.amp, FAKE_UUID_1, config)
|
||||
@@ -642,8 +655,10 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
def test_upload_config_server_error(self, m):
|
||||
config = '{"name": "bad_config"}'
|
||||
m.put(
|
||||
"{base}/listeners/{listener_id}/haproxy".format(
|
||||
base=self.base_url, listener_id=FAKE_UUID_1),
|
||||
"{base}/listeners/{"
|
||||
"amphora_id}/{listener_id}/haproxy".format(
|
||||
amphora_id=self.amp.id, base=self.base_url,
|
||||
listener_id=FAKE_UUID_1),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.upload_config,
|
||||
self.amp, FAKE_UUID_1, config)
|
||||
@@ -652,8 +667,10 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
def test_upload_config_service_unavailable(self, m):
|
||||
config = '{"name": "bad_config"}'
|
||||
m.put(
|
||||
"{base}/listeners/{listener_id}/haproxy".format(
|
||||
base=self.base_url, listener_id=FAKE_UUID_1),
|
||||
"{base}/listeners/{"
|
||||
"amphora_id}/{listener_id}/haproxy".format(
|
||||
amphora_id=self.amp.id, base=self.base_url,
|
||||
listener_id=FAKE_UUID_1),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.upload_config,
|
||||
self.amp, FAKE_UUID_1, config)
|
||||
@@ -673,3 +690,36 @@ class TestAmphoraAPIClientTest(base.TestCase):
|
||||
)
|
||||
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)
|
||||
)
|
||||
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, 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 = '10.0.0.1'
|
||||
m.get("{base}/interface/{ip_addr}".format(base=self.base_url,
|
||||
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,
|
||||
status_code=500, reason='FAIL', json='FAIL')
|
||||
self.assertRaises(exc.InternalServerError,
|
||||
self.driver.get_interface,
|
||||
self.amp, ip_addr)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from octavia.amphorae.drivers.keepalived.jinja import jinja_cfg
|
||||
from octavia.common import constants
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
|
||||
class TestVRRPRestDriver(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestVRRPRestDriver, self).setUp()
|
||||
self.templater = jinja_cfg.KeepalivedJinjaTemplater()
|
||||
|
||||
self.amphora1 = mock.MagicMock()
|
||||
self.amphora1.vrrp_ip = '10.0.0.1'
|
||||
self.amphora1.role = constants.ROLE_MASTER
|
||||
self.amphora1.vrrp_interface = 'eth1'
|
||||
self.amphora1.vrrp_id = 1
|
||||
self.amphora1.vrrp_priority = 100
|
||||
|
||||
self.amphora2 = mock.MagicMock()
|
||||
self.amphora2.vrrp_ip = '10.0.0.2'
|
||||
self.amphora2.role = constants.ROLE_BACKUP
|
||||
self.amphora2.vrrp_interface = 'eth1'
|
||||
self.amphora2.vrrp_id = 1
|
||||
self.amphora2.vrrp_priority = 90
|
||||
|
||||
self.lb = mock.MagicMock()
|
||||
self.lb.amphorae = [self.amphora1, self.amphora2]
|
||||
self.lb.vrrp_group.vrrp_group_name = 'TESTGROUP'
|
||||
self.lb.vrrp_group.vrrp_auth_type = constants.VRRP_AUTH_DEFAULT
|
||||
self.lb.vrrp_group.vrrp_auth_pass = 'TESTPASSWORD'
|
||||
self.lb.vip.ip_address = '10.1.0.5'
|
||||
self.lb.vrrp_group.advert_int = 10
|
||||
|
||||
self.ref_conf = ("\n"
|
||||
"\n"
|
||||
"vrrp_script check_script {\n"
|
||||
" script /tmp/test/vrrp/check_script.sh\n"
|
||||
" interval 5\n"
|
||||
" fall 2\n"
|
||||
" rise 2\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"vrrp_instance TESTGROUP {\n"
|
||||
" state MASTER\n"
|
||||
" interface eth1\n"
|
||||
" virtual_router_id 1\n"
|
||||
" priority 100\n"
|
||||
" garp_master_refresh 5\n"
|
||||
" garp_master_refresh_repeat 2\n"
|
||||
" advert_int 10\n"
|
||||
" authentication {\n"
|
||||
" auth_type PASS\n"
|
||||
" auth_pass TESTPASSWORD\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" unicast_src_ip 10.0.0.1\n"
|
||||
" unicast_peer {\n"
|
||||
" 10.0.0.2\n"
|
||||
"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" virtual_ipaddress {\n"
|
||||
" 10.1.0.5\n"
|
||||
" }\n"
|
||||
" track_script {\n"
|
||||
" check_script\n"
|
||||
" }\n"
|
||||
"}\n")
|
||||
|
||||
def test_build_keepalived_config(self):
|
||||
cfg.CONF.set_override('vrrp_garp_refresh_interval', 5,
|
||||
group='keepalived_vrrp')
|
||||
cfg.CONF.set_override('vrrp_garp_refresh_count', 2,
|
||||
group='keepalived_vrrp')
|
||||
cfg.CONF.set_override('vrrp_check_interval', 5,
|
||||
group='keepalived_vrrp')
|
||||
cfg.CONF.set_override('vrrp_fail_count', 2,
|
||||
group='keepalived_vrrp')
|
||||
cfg.CONF.set_override('vrrp_success_count', 2,
|
||||
group='keepalived_vrrp')
|
||||
cfg.CONF.set_override('base_path', '/tmp/test/',
|
||||
group='haproxy_amphora')
|
||||
config = self.templater.build_keepalived_config(self.lb, self.amphora1)
|
||||
self.assertEqual(self.ref_conf, config)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from octavia.amphorae.drivers.keepalived import vrrp_rest_driver
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
|
||||
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.FAKE_CONFIG = 'FAKE CONFIG'
|
||||
self.lb_mock = mock.MagicMock()
|
||||
self.amphora_mock = mock.MagicMock()
|
||||
self.lb_mock.amphorae = [self.amphora_mock]
|
||||
super(TestVRRPRestDriver, self).setUp()
|
||||
|
||||
@mock.patch('octavia.amphorae.drivers.keepalived.jinja.'
|
||||
'jinja_cfg.KeepalivedJinjaTemplater.build_keepalived_config')
|
||||
def test_update_vrrp_conf(self, mock_templater):
|
||||
|
||||
mock_templater.return_value = self.FAKE_CONFIG
|
||||
|
||||
self.keepalived_mixin.update_vrrp_conf(self.lb_mock)
|
||||
|
||||
self.client.upload_vrrp_config.assert_called_once_with(
|
||||
self.amphora_mock,
|
||||
self.FAKE_CONFIG)
|
||||
|
||||
def test_stop_vrrp_service(self):
|
||||
|
||||
self.keepalived_mixin.stop_vrrp_service(self.lb_mock)
|
||||
|
||||
self.client.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)
|
||||
|
||||
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)
|
||||
44
octavia/tests/unit/cmd/test_haproxy_vrrp_check.py
Normal file
44
octavia/tests/unit/cmd/test_haproxy_vrrp_check.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from octavia.cmd import haproxy_vrrp_check
|
||||
from octavia.tests.unit import base
|
||||
|
||||
|
||||
class TestHAproxyVRRPCheckCMD(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHAproxyVRRPCheckCMD, self).setUp()
|
||||
|
||||
@mock.patch('socket.socket')
|
||||
def test_health_check(self, mock_socket):
|
||||
socket_mock = mock.MagicMock()
|
||||
mock_socket.return_value = socket_mock
|
||||
recv_mock = mock.MagicMock()
|
||||
recv_mock.side_effect = ['1', Exception('BREAK')]
|
||||
socket_mock.recv = recv_mock
|
||||
|
||||
self.assertRaisesRegexp(Exception, 'BREAK',
|
||||
haproxy_vrrp_check.health_check,
|
||||
'10.0.0.1')
|
||||
|
||||
@mock.patch('octavia.cmd.haproxy_vrrp_check.health_check')
|
||||
@mock.patch('sys.argv')
|
||||
@mock.patch('sys.exit')
|
||||
def test_main(self, mock_exit, mock_argv, mock_health_check):
|
||||
mock_health_check.side_effect = [1, Exception('FAIL')]
|
||||
haproxy_vrrp_check.main()
|
||||
mock_exit.assert_called_once_with(1)
|
||||
@@ -15,6 +15,16 @@
|
||||
|
||||
import collections
|
||||
|
||||
|
||||
def sample_amphora_tuple():
|
||||
amphora = collections.namedtuple('amphora', 'id, load_balancer_id, '
|
||||
'compute_id, status,'
|
||||
'lb_network_ip, vrrp_ip')
|
||||
return amphora(id='sample_amp_id_1', load_balancer_id='sample_lb_id_1',
|
||||
compute_id='sample_compute_id_1', status='ACTIVE',
|
||||
lb_network_ip='10.0.0.1',
|
||||
vrrp_ip='10.0.0.2')
|
||||
|
||||
RET_PERSISTENCE = {
|
||||
'type': 'HTTP_COOKIE',
|
||||
'cookie_name': 'HTTP_COOKIE'}
|
||||
@@ -57,7 +67,8 @@ RET_POOL = {
|
||||
'health_monitor': RET_MONITOR,
|
||||
'session_persistence': RET_PERSISTENCE,
|
||||
'enabled': True,
|
||||
'operating_status': 'ACTIVE'}
|
||||
'operating_status': 'ACTIVE',
|
||||
'stick_size': '10k'}
|
||||
|
||||
RET_DEF_TLS_CONT = {'id': 'cont_id_1', 'allencompassingpem': 'imapem',
|
||||
'primary_cn': 'FakeCn'}
|
||||
@@ -72,7 +83,10 @@ RET_LISTENER = {
|
||||
'protocol': 'HTTP',
|
||||
'protocol_mode': 'http',
|
||||
'default_pool': RET_POOL,
|
||||
'connection_limit': 98}
|
||||
'connection_limit': 98,
|
||||
'amphorae': [sample_amphora_tuple()],
|
||||
'peer_port': 1024,
|
||||
'topology': 'SINGLE'}
|
||||
|
||||
RET_LISTENER_TLS = {
|
||||
'id': 'sample_listener_id_1',
|
||||
@@ -102,7 +116,8 @@ RET_LISTENER_TLS_SNI = {
|
||||
RET_LB = {
|
||||
'name': 'test-lb',
|
||||
'vip_address': '10.0.0.2',
|
||||
'listener': RET_LISTENER}
|
||||
'listener': RET_LISTENER,
|
||||
'topology': 'SINGLE'}
|
||||
|
||||
RET_LB_TLS = {
|
||||
'name': 'test-lb',
|
||||
@@ -116,8 +131,10 @@ RET_LB_TLS_SNI = {
|
||||
|
||||
|
||||
def sample_loadbalancer_tuple(proto=None, monitor=True, persistence=True,
|
||||
persistence_type=None, tls=False, sni=False):
|
||||
persistence_type=None, tls=False, sni=False,
|
||||
topology=None):
|
||||
proto = 'HTTP' if proto is None else proto
|
||||
topology = 'SINGLE' if topology is None else topology
|
||||
in_lb = collections.namedtuple(
|
||||
'load_balancer', 'id, name, protocol, vip, listeners, amphorae')
|
||||
return in_lb(
|
||||
@@ -125,6 +142,7 @@ def sample_loadbalancer_tuple(proto=None, monitor=True, persistence=True,
|
||||
name='test-lb',
|
||||
protocol=proto,
|
||||
vip=sample_vip_tuple(),
|
||||
topology=topology,
|
||||
listeners=[sample_listener_tuple(proto=proto, monitor=monitor,
|
||||
persistence=persistence,
|
||||
persistence_type=persistence_type,
|
||||
@@ -133,38 +151,60 @@ def sample_loadbalancer_tuple(proto=None, monitor=True, persistence=True,
|
||||
)
|
||||
|
||||
|
||||
def sample_listener_loadbalancer_tuple(proto=None):
|
||||
def sample_listener_loadbalancer_tuple(proto=None, topology=None):
|
||||
proto = 'HTTP' if proto is None else proto
|
||||
topology = 'SINGLE' if topology is None else topology
|
||||
in_lb = collections.namedtuple(
|
||||
'load_balancer', 'id, name, protocol, vip, amphorae')
|
||||
'load_balancer', 'id, name, protocol, vip, amphorae, topology')
|
||||
return in_lb(
|
||||
id='sample_loadbalancer_id_1',
|
||||
name='test-lb',
|
||||
protocol=proto,
|
||||
vip=sample_vip_tuple(),
|
||||
amphorae=[sample_amphora_tuple()]
|
||||
amphorae=[sample_amphora_tuple()],
|
||||
topology=topology
|
||||
)
|
||||
|
||||
|
||||
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, persistence=True,
|
||||
persistence_type=None, tls=False, sni=False):
|
||||
persistence_type=None, tls=False, sni=False,
|
||||
peer_port=None, topology=None):
|
||||
proto = 'HTTP' if proto is None 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
|
||||
in_listener = collections.namedtuple(
|
||||
'listener', 'id, protocol_port, protocol, default_pool, '
|
||||
'connection_limit, tls_certificate_id, '
|
||||
'sni_container_ids, default_tls_container, '
|
||||
'sni_containers, load_balancer')
|
||||
'sni_containers, load_balancer, peer_port')
|
||||
return in_listener(
|
||||
id='sample_listener_id_1',
|
||||
protocol_port=port,
|
||||
protocol=proto,
|
||||
load_balancer=sample_listener_loadbalancer_tuple(proto=proto),
|
||||
load_balancer=sample_listener_loadbalancer_tuple(proto=proto,
|
||||
topology=topology),
|
||||
peer_port=peer_port,
|
||||
default_pool=sample_pool_tuple(
|
||||
proto=proto, monitor=monitor, persistence=persistence,
|
||||
persistence_type=persistence_type),
|
||||
@@ -270,16 +310,7 @@ def sample_health_monitor_tuple(proto='HTTP'):
|
||||
expected_codes='418', enabled=True)
|
||||
|
||||
|
||||
def sample_amphora_tuple():
|
||||
amphora = collections.namedtuple('amphora', 'id, load_balancer_id, '
|
||||
'compute_id, status,'
|
||||
'lb_network_ip')
|
||||
return amphora(id='sample_amp_id_1', load_balancer_id='sample_lb_id_1',
|
||||
compute_id='sample_compute_id_1', status='ACTIVE',
|
||||
lb_network_ip='10.0.0.1')
|
||||
|
||||
|
||||
def sample_base_expected_config(frontend=None, backend=None):
|
||||
def sample_base_expected_config(frontend=None, backend=None, peers=None):
|
||||
if frontend is None:
|
||||
frontend = ("frontend sample_listener_id_1\n"
|
||||
" option tcplog\n"
|
||||
@@ -300,6 +331,8 @@ def sample_base_expected_config(frontend=None, backend=None):
|
||||
"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")
|
||||
if peers is None:
|
||||
peers = ("\n\n")
|
||||
return ("# Configuration for test-lb\n"
|
||||
"global\n"
|
||||
" daemon\n"
|
||||
@@ -315,4 +348,4 @@ def sample_base_expected_config(frontend=None, backend=None):
|
||||
" option redispatch\n"
|
||||
" timeout connect 5000\n"
|
||||
" timeout client 50000\n"
|
||||
" timeout server 50000\n\n" + frontend + backend)
|
||||
" timeout server 50000\n\n" + peers + frontend + backend)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as oslo_fixture
|
||||
from taskflow.patterns import linear_flow as flow
|
||||
@@ -65,41 +66,85 @@ class TestAmphoraFlows(base.TestCase):
|
||||
self.assertEqual(0, len(amp_flow.requires))
|
||||
|
||||
def test_get_create_amphora_for_lb_flow(self):
|
||||
cfg.CONF.set_override('amphora_driver', 'amphora_haproxy_ssh_driver',
|
||||
group='controller_worker')
|
||||
|
||||
amp_flow = self.AmpFlow.get_create_amphora_for_lb_flow()
|
||||
amp_flow = self.AmpFlow._get_create_amp_for_lb_subflow(
|
||||
'SOMEPREFIX', constants.ROLE_STANDALONE)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.LOADBALANCER, amp_flow.provides)
|
||||
self.assertIn(constants.VIP, amp_flow.provides)
|
||||
self.assertIn(constants.AMPS_DATA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
|
||||
self.assertEqual(8, len(amp_flow.provides))
|
||||
self.assertEqual(4, len(amp_flow.provides))
|
||||
self.assertEqual(1, len(amp_flow.requires))
|
||||
|
||||
def test_get_cert_create_amphora_for_lb_flow(self):
|
||||
cfg.CONF.set_override('amphora_driver', 'amphora_haproxy_rest_driver',
|
||||
group='controller_worker')
|
||||
self.AmpFlow = amphora_flows.AmphoraFlows()
|
||||
amp_flow = self.AmpFlow.get_create_amphora_for_lb_flow()
|
||||
|
||||
amp_flow = self.AmpFlow._get_create_amp_for_lb_subflow(
|
||||
'SOMEPREFIX', constants.ROLE_STANDALONE)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.LOADBALANCER, amp_flow.provides)
|
||||
self.assertIn(constants.VIP, amp_flow.provides)
|
||||
self.assertIn(constants.AMPS_DATA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
self.assertIn(constants.SERVER_PEM, amp_flow.provides)
|
||||
|
||||
self.assertEqual(9, len(amp_flow.provides))
|
||||
self.assertEqual(5, len(amp_flow.provides))
|
||||
self.assertEqual(1, len(amp_flow.requires))
|
||||
|
||||
def test_get_cert_master_create_amphora_for_lb_flow(self):
|
||||
cfg.CONF.set_override('amphora_driver', 'amphora_haproxy_rest_driver',
|
||||
group='controller_worker')
|
||||
self.AmpFlow = amphora_flows.AmphoraFlows()
|
||||
|
||||
amp_flow = self.AmpFlow._get_create_amp_for_lb_subflow(
|
||||
'SOMEPREFIX', constants.ROLE_MASTER)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
self.assertIn(constants.SERVER_PEM, amp_flow.provides)
|
||||
|
||||
self.assertEqual(5, len(amp_flow.provides))
|
||||
self.assertEqual(1, len(amp_flow.requires))
|
||||
|
||||
def test_get_cert_backup_create_amphora_for_lb_flow(self):
|
||||
cfg.CONF.set_override('amphora_driver', 'amphora_haproxy_rest_driver',
|
||||
group='controller_worker')
|
||||
self.AmpFlow = amphora_flows.AmphoraFlows()
|
||||
|
||||
amp_flow = self.AmpFlow._get_create_amp_for_lb_subflow(
|
||||
'SOMEPREFIX', constants.ROLE_BACKUP)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
self.assertIn(constants.SERVER_PEM, amp_flow.provides)
|
||||
|
||||
self.assertEqual(5, len(amp_flow.provides))
|
||||
self.assertEqual(1, len(amp_flow.requires))
|
||||
|
||||
def test_get_delete_amphora_flow(self):
|
||||
@@ -113,6 +158,24 @@ class TestAmphoraFlows(base.TestCase):
|
||||
self.assertEqual(0, len(amp_flow.provides))
|
||||
self.assertEqual(1, len(amp_flow.requires))
|
||||
|
||||
def test_allocate_amp_to_lb_decider(self):
|
||||
history = mock.MagicMock()
|
||||
values = mock.MagicMock(side_effect=[['TEST'], [None]])
|
||||
history.values = values
|
||||
result = self.AmpFlow._allocate_amp_to_lb_decider(history)
|
||||
self.assertTrue(result)
|
||||
result = self.AmpFlow._allocate_amp_to_lb_decider(history)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_create_new_amp_for_lb_decider(self):
|
||||
history = mock.MagicMock()
|
||||
values = mock.MagicMock(side_effect=[[None], ['TEST']])
|
||||
history.values = values
|
||||
result = self.AmpFlow._create_new_amp_for_lb_decider(history)
|
||||
self.assertTrue(result)
|
||||
result = self.AmpFlow._create_new_amp_for_lb_decider(history)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_get_failover_flow(self):
|
||||
|
||||
amp_flow = self.AmpFlow.get_failover_flow()
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestListenerFlows(base.TestCase):
|
||||
self.assertIn(constants.VIP, listener_flow.requires)
|
||||
|
||||
self.assertEqual(3, len(listener_flow.requires))
|
||||
self.assertEqual(0, len(listener_flow.provides))
|
||||
self.assertEqual(1, len(listener_flow.provides))
|
||||
|
||||
def test_get_delete_listener_flow(self):
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
#
|
||||
|
||||
from taskflow.patterns import linear_flow as flow
|
||||
from taskflow.patterns import unordered_flow
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.controller.worker.flows import load_balancer_flows
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
@@ -28,21 +30,29 @@ class TestLoadBalancerFlows(base.TestCase):
|
||||
super(TestLoadBalancerFlows, self).setUp()
|
||||
|
||||
def test_get_create_load_balancer_flow(self):
|
||||
amp_flow = self.LBFlow.get_create_load_balancer_flow(
|
||||
constants.TOPOLOGY_SINGLE)
|
||||
self.assertIsInstance(amp_flow, unordered_flow.Flow)
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
|
||||
lb_flow = self.LBFlow.get_create_load_balancer_flow()
|
||||
def test_get_create_active_standby_load_balancer_flow(self):
|
||||
amp_flow = self.LBFlow.get_create_load_balancer_flow(
|
||||
constants.TOPOLOGY_ACTIVE_STANDBY)
|
||||
self.assertIsInstance(amp_flow, unordered_flow.Flow)
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
self.assertIn(constants.AMPHORA, amp_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_ID, amp_flow.provides)
|
||||
self.assertIn(constants.COMPUTE_OBJ, amp_flow.provides)
|
||||
|
||||
self.assertIsInstance(lb_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.AMPHORA, lb_flow.provides)
|
||||
self.assertIn(constants.AMPHORA_ID, lb_flow.provides)
|
||||
self.assertIn(constants.VIP, lb_flow.provides)
|
||||
self.assertIn(constants.AMPS_DATA, lb_flow.provides)
|
||||
self.assertIn(constants.LOADBALANCER, lb_flow.provides)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, lb_flow.requires)
|
||||
|
||||
self.assertEqual(6, len(lb_flow.provides))
|
||||
self.assertEqual(1, len(lb_flow.requires))
|
||||
def test_get_create_bogus_topology_load_balancer_flow(self):
|
||||
self.assertRaises(exceptions.InvalidTopology,
|
||||
self.LBFlow.get_create_load_balancer_flow,
|
||||
'BOGUS')
|
||||
|
||||
def test_get_delete_load_balancer_flow(self):
|
||||
|
||||
@@ -81,3 +91,41 @@ class TestLoadBalancerFlows(base.TestCase):
|
||||
|
||||
self.assertEqual(0, len(lb_flow.provides))
|
||||
self.assertEqual(2, len(lb_flow.requires))
|
||||
|
||||
def test_get_post_lb_amp_association_flow(self):
|
||||
amp_flow = self.LBFlow.get_post_lb_amp_association_flow(
|
||||
'123', constants.TOPOLOGY_SINGLE)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
self.assertIn(constants.UPDATE_DICT, amp_flow.requires)
|
||||
self.assertIn(constants.LOADBALANCER, amp_flow.provides)
|
||||
|
||||
self.assertEqual(4, len(amp_flow.provides))
|
||||
self.assertEqual(2, len(amp_flow.requires))
|
||||
|
||||
# Test Active/Standby path
|
||||
amp_flow = self.LBFlow.get_post_lb_amp_association_flow(
|
||||
'123', constants.TOPOLOGY_ACTIVE_STANDBY)
|
||||
|
||||
self.assertIsInstance(amp_flow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires)
|
||||
self.assertIn(constants.UPDATE_DICT, amp_flow.requires)
|
||||
self.assertIn(constants.LOADBALANCER, amp_flow.provides)
|
||||
|
||||
self.assertEqual(4, len(amp_flow.provides))
|
||||
self.assertEqual(2, len(amp_flow.requires))
|
||||
|
||||
def test_get_vrrp_subflow(self):
|
||||
vrrp_subflow = self.LBFlow._get_vrrp_subflow('123')
|
||||
|
||||
self.assertIsInstance(vrrp_subflow, flow.Flow)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER, vrrp_subflow.provides)
|
||||
|
||||
self.assertIn(constants.LOADBALANCER, vrrp_subflow.requires)
|
||||
|
||||
self.assertEqual(1, len(vrrp_subflow.provides))
|
||||
self.assertEqual(1, len(vrrp_subflow.requires))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
from taskflow.types import failure
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.controller.worker.tasks import amphora_driver_tasks
|
||||
@@ -37,11 +38,14 @@ _amphorae_mock = [_amphora_mock]
|
||||
_network_mock = mock.MagicMock()
|
||||
_port_mock = mock.MagicMock()
|
||||
_ports_mock = [_port_mock]
|
||||
_session_mock = mock.MagicMock()
|
||||
|
||||
|
||||
@mock.patch('octavia.db.repositories.AmphoraRepository.update')
|
||||
@mock.patch('octavia.db.repositories.ListenerRepository.update')
|
||||
@mock.patch('octavia.db.api.get_session', return_value='TEST')
|
||||
@mock.patch('octavia.db.repositories.ListenerRepository.get',
|
||||
return_value=_listener_mock)
|
||||
@mock.patch('octavia.db.api.get_session', return_value=_session_mock)
|
||||
@mock.patch('octavia.controller.worker.tasks.amphora_driver_tasks.LOG')
|
||||
@mock.patch('oslo_utils.uuidutils.generate_uuid', return_value=AMP_ID)
|
||||
@mock.patch('stevedore.driver.DriverManager.driver')
|
||||
@@ -58,6 +62,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -70,7 +75,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test the revert
|
||||
amp = listener_update_obj.revert(_listener_mock)
|
||||
repo.ListenerRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=LISTENER_ID,
|
||||
provisioning_status=constants.ERROR)
|
||||
self.assertIsNone(amp)
|
||||
@@ -80,6 +85,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -92,7 +98,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test the revert
|
||||
amp = listener_stop_obj.revert(_listener_mock)
|
||||
repo.ListenerRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=LISTENER_ID,
|
||||
provisioning_status=constants.ERROR)
|
||||
self.assertIsNone(amp)
|
||||
@@ -102,6 +108,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -114,7 +121,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test the revert
|
||||
amp = listener_start_obj.revert(_listener_mock)
|
||||
repo.ListenerRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=LISTENER_ID,
|
||||
provisioning_status=constants.ERROR)
|
||||
self.assertIsNone(amp)
|
||||
@@ -124,6 +131,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -136,7 +144,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test the revert
|
||||
amp = listener_delete_obj.revert(_listener_mock)
|
||||
repo.ListenerRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=LISTENER_ID,
|
||||
provisioning_status=constants.ERROR)
|
||||
self.assertIsNone(amp)
|
||||
@@ -146,6 +154,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -160,6 +169,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -175,6 +185,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -187,7 +198,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test revert
|
||||
amp = amphora_finalize_obj.revert(None, _amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=AMP_ID,
|
||||
status=constants.ERROR)
|
||||
self.assertIsNone(amp)
|
||||
@@ -197,6 +208,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -210,7 +222,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test revert
|
||||
amp = amphora_post_network_plug_obj.revert(None, _amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=AMP_ID,
|
||||
status=constants.ERROR)
|
||||
|
||||
@@ -220,6 +232,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
mock_driver.get_network.return_value = _network_mock
|
||||
@@ -228,8 +241,10 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
_LB_mock.amphorae = [_amphora_mock]
|
||||
amphora_post_network_plug_obj = (amphora_driver_tasks.
|
||||
AmphoraePostNetworkPlug())
|
||||
|
||||
port_mock = mock.Mock()
|
||||
_deltas_mock = {_amphora_mock.id: [port_mock]}
|
||||
|
||||
amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock)
|
||||
|
||||
(mock_driver.post_network_plug.
|
||||
@@ -239,7 +254,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
amp = amphora_post_network_plug_obj.revert(None, _LB_mock,
|
||||
_deltas_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=AMP_ID,
|
||||
status=constants.ERROR)
|
||||
|
||||
@@ -252,6 +267,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
|
||||
@@ -265,9 +281,9 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
# Test revert
|
||||
amp = amphora_post_vip_plug_obj.revert(None, _LB_mock)
|
||||
repo.LoadBalancerRepository.update.assert_called_once_with(
|
||||
'TEST',
|
||||
_session_mock,
|
||||
id=LB_ID,
|
||||
status=constants.ERROR)
|
||||
provisioning_status=constants.ERROR)
|
||||
|
||||
self.assertIsNone(amp)
|
||||
|
||||
@@ -276,6 +292,7 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
pem_file_mock = 'test-perm-file'
|
||||
@@ -284,3 +301,71 @@ class TestAmphoraDriverTasks(base.TestCase):
|
||||
|
||||
mock_driver.upload_cert_amp.assert_called_once_with(
|
||||
_amphora_mock, pem_file_mock)
|
||||
|
||||
def test_amphora_update_vrrp_interface(self,
|
||||
mock_driver,
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
_LB_mock.amphorae = _amphorae_mock
|
||||
amphora_update_vrrp_interface_obj = (
|
||||
amphora_driver_tasks.AmphoraUpdateVRRPInterface())
|
||||
amphora_update_vrrp_interface_obj.execute(_LB_mock)
|
||||
mock_driver.get_vrrp_interface.assert_called_once_with(_amphora_mock)
|
||||
|
||||
mock_driver.reset_mock()
|
||||
|
||||
_LB_mock.amphorae = _amphorae_mock
|
||||
amphora_update_vrrp_interface_obj.revert("BADRESULT", _LB_mock)
|
||||
mock_amphora_repo_update.assert_called_with(_session_mock,
|
||||
_amphora_mock.id,
|
||||
vrrp_interface=None)
|
||||
|
||||
mock_driver.reset_mock()
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
failure_obj = failure.Failure.from_exception(Exception("TESTEXCEPT"))
|
||||
amphora_update_vrrp_interface_obj.revert(failure_obj, _LB_mock)
|
||||
self.assertFalse(mock_amphora_repo_update.called)
|
||||
|
||||
def test_amphora_vrrp_update(self,
|
||||
mock_driver,
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
amphora_vrrp_update_obj = (
|
||||
amphora_driver_tasks.AmphoraVRRPUpdate())
|
||||
amphora_vrrp_update_obj.execute(_LB_mock)
|
||||
mock_driver.update_vrrp_conf.assert_called_once_with(_LB_mock)
|
||||
|
||||
def test_amphora_vrrp_stop(self,
|
||||
mock_driver,
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
amphora_vrrp_stop_obj = (
|
||||
amphora_driver_tasks.AmphoraVRRPStop())
|
||||
amphora_vrrp_stop_obj.execute(_LB_mock)
|
||||
mock_driver.stop_vrrp_service.assert_called_once_with(_LB_mock)
|
||||
|
||||
def test_amphora_vrrp_start(self,
|
||||
mock_driver,
|
||||
mock_generate_uuid,
|
||||
mock_log,
|
||||
mock_get_session,
|
||||
mock_listener_repo_get,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update):
|
||||
amphora_vrrp_start_obj = (
|
||||
amphora_driver_tasks.AmphoraVRRPStart())
|
||||
amphora_vrrp_start_obj.execute(_LB_mock)
|
||||
mock_driver.start_vrrp_service.assert_called_once_with(_LB_mock)
|
||||
|
||||
@@ -18,7 +18,6 @@ from oslo_utils import uuidutils
|
||||
from taskflow.types import failure
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.controller.worker.tasks import database_tasks
|
||||
from octavia.db import repositories as repo
|
||||
import octavia.tests.unit.base as base
|
||||
@@ -297,6 +296,27 @@ class TestDatabaseTasks(base.TestCase):
|
||||
|
||||
self.assertEqual(_loadbalancer_mock, lb)
|
||||
|
||||
@mock.patch('octavia.db.repositories.ListenerRepository.get',
|
||||
return_value=_listener_mock)
|
||||
def test_reload_listener(self,
|
||||
mock_listener_get,
|
||||
mock_generate_uuid,
|
||||
mock_LOG,
|
||||
mock_get_session,
|
||||
mock_loadbalancer_repo_update,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update,
|
||||
mock_amphora_repo_delete):
|
||||
|
||||
reload_listener = database_tasks.ReloadListener()
|
||||
listener = reload_listener.execute(_listener_mock)
|
||||
|
||||
repo.ListenerRepository.get.assert_called_once_with(
|
||||
'TEST',
|
||||
id=LISTENER_ID)
|
||||
|
||||
self.assertEqual(_listener_mock, listener)
|
||||
|
||||
@mock.patch('octavia.db.repositories.AmphoraRepository.'
|
||||
'allocate_and_associate',
|
||||
side_effect=[_amphora_mock, None])
|
||||
@@ -317,10 +337,11 @@ class TestDatabaseTasks(base.TestCase):
|
||||
'TEST',
|
||||
LB_ID)
|
||||
|
||||
assert amp_id == _amphora_mock.id
|
||||
self.assertEqual(_amphora_mock.id, amp_id)
|
||||
|
||||
self.assertRaises(exceptions.NoReadyAmphoraeException,
|
||||
map_lb_to_amp.execute, self.loadbalancer_mock.id)
|
||||
amp_id = map_lb_to_amp.execute(self.loadbalancer_mock.id)
|
||||
|
||||
self.assertIsNone(amp_id)
|
||||
|
||||
@mock.patch('octavia.db.repositories.AmphoraRepository.get',
|
||||
return_value=_amphora_mock)
|
||||
@@ -919,3 +940,102 @@ class TestDatabaseTasks(base.TestCase):
|
||||
'TEST',
|
||||
POOL_ID,
|
||||
enabled=0)
|
||||
|
||||
def test_mark_amphora_role_indb(self,
|
||||
mock_generate_uuid,
|
||||
mock_LOG,
|
||||
mock_get_session,
|
||||
mock_loadbalancer_repo_update,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update,
|
||||
mock_amphora_repo_delete):
|
||||
|
||||
mark_amp_master_indb = database_tasks.MarkAmphoraMasterInDB()
|
||||
mark_amp_master_indb.execute(_amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role='MASTER',
|
||||
vrrp_priority=constants.ROLE_MASTER_PRIORITY)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
mark_amp_master_indb.revert("BADRESULT", _amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role=None, vrrp_priority=None)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
failure_obj = failure.Failure.from_exception(Exception("TESTEXCEPT"))
|
||||
mark_amp_master_indb.revert(failure_obj, _amphora_mock)
|
||||
self.assertFalse(repo.AmphoraRepository.update.called)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
mark_amp_backup_indb = database_tasks.MarkAmphoraBackupInDB()
|
||||
mark_amp_backup_indb.execute(_amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role='BACKUP',
|
||||
vrrp_priority=constants.ROLE_BACKUP_PRIORITY)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
mark_amp_backup_indb.revert("BADRESULT", _amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role=None, vrrp_priority=None)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
mark_amp_standalone_indb = database_tasks.MarkAmphoraStandAloneInDB()
|
||||
mark_amp_standalone_indb.execute(_amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role='STANDALONE',
|
||||
vrrp_priority=None)
|
||||
|
||||
mock_amphora_repo_update.reset_mock()
|
||||
|
||||
mark_amp_standalone_indb.revert("BADRESULT", _amphora_mock)
|
||||
repo.AmphoraRepository.update.assert_called_once_with(
|
||||
'TEST', AMP_ID, role=None, vrrp_priority=None)
|
||||
|
||||
@mock.patch('octavia.db.repositories.VRRPGroupRepository.create')
|
||||
def test_create_vrrp_group_for_lb(self,
|
||||
mock_vrrp_group_create,
|
||||
mock_generate_uuid,
|
||||
mock_LOG,
|
||||
mock_get_session,
|
||||
mock_loadbalancer_repo_update,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update,
|
||||
mock_amphora_repo_delete):
|
||||
|
||||
create_vrrp_group = database_tasks.CreateVRRPGroupForLB()
|
||||
create_vrrp_group.execute(_loadbalancer_mock)
|
||||
mock_vrrp_group_create.assert_called_once_with(
|
||||
'TEST', load_balancer_id=LB_ID,
|
||||
vrrp_group_name=LB_ID.replace('-', ''),
|
||||
vrrp_auth_type=constants.VRRP_AUTH_DEFAULT,
|
||||
vrrp_auth_pass=mock_generate_uuid.return_value.replace('-',
|
||||
'')[0:7],
|
||||
advert_int=1)
|
||||
|
||||
@mock.patch('octavia.db.repositories.ListenerRepository.get')
|
||||
def test_allocate_listener_peer_port(self,
|
||||
mock_listener_repo_get,
|
||||
mock_generate_uuid,
|
||||
mock_LOG,
|
||||
mock_get_session,
|
||||
mock_loadbalancer_repo_update,
|
||||
mock_listener_repo_update,
|
||||
mock_amphora_repo_update,
|
||||
mock_amphora_repo_delete):
|
||||
allocate_listener_peer_port = database_tasks.AllocateListenerPeerPort()
|
||||
allocate_listener_peer_port.execute(_listener_mock)
|
||||
mock_listener_repo_update.assert_called_once_with(
|
||||
'TEST', _listener_mock.id,
|
||||
peer_port=constants.HAPROXY_BASE_PEER_PORT)
|
||||
|
||||
mock_listener_repo_update.reset_mock()
|
||||
|
||||
allocate_listener_peer_port.revert(_listener_mock)
|
||||
mock_listener_repo_update.assert_called_once_with(
|
||||
'TEST', _listener_mock.id,
|
||||
peer_port=None)
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
#
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from octavia.common import base_taskflow
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.controller.worker import controller_worker
|
||||
import octavia.tests.unit.base as base
|
||||
|
||||
@@ -46,6 +46,8 @@ _create_map_flow_mock = mock.MagicMock()
|
||||
_amphora_mock.load_balancer_id = LB_ID
|
||||
_amphora_mock.id = AMP_ID
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@mock.patch('octavia.db.repositories.AmphoraRepository.get',
|
||||
return_value=_amphora_mock)
|
||||
@@ -337,19 +339,14 @@ class TestControllerWorker(base.TestCase):
|
||||
|
||||
_flow_mock.run.assert_called_once_with()
|
||||
|
||||
@mock.patch('octavia.controller.worker.flows.load_balancer_flows.'
|
||||
'LoadBalancerFlows.get_post_lb_amp_association_flow')
|
||||
@mock.patch('octavia.controller.worker.flows.load_balancer_flows.'
|
||||
'LoadBalancerFlows.get_create_load_balancer_flow',
|
||||
return_value=_flow_mock)
|
||||
@mock.patch('octavia.controller.worker.flows.'
|
||||
'amphora_flows.AmphoraFlows.get_create_amphora_flow',
|
||||
return_value='TEST2')
|
||||
@mock.patch('octavia.controller.worker.flows.'
|
||||
'amphora_flows.AmphoraFlows.get_create_amphora_for_lb_flow',
|
||||
return_value='TEST2')
|
||||
def test_create_load_balancer(self,
|
||||
mock_get_create_amp_for_lb_flow,
|
||||
mock_get_create_amp_flow,
|
||||
mock_get_create_lb_flow,
|
||||
mock_get_create_load_balancer_flow,
|
||||
mock_get_get_post_lb_amp_association_flow,
|
||||
mock_api_get_session,
|
||||
mock_dyn_log_listener,
|
||||
mock_taskflow_load,
|
||||
@@ -360,50 +357,54 @@ class TestControllerWorker(base.TestCase):
|
||||
mock_health_mon_repo_get,
|
||||
mock_amp_repo_get):
|
||||
|
||||
# Test code path with an existing READY amphora
|
||||
# Test the code path with an SINGLE topology
|
||||
CONF.set_override(group='controller_worker',
|
||||
name='loadbalancer_topology',
|
||||
override=constants.TOPOLOGY_SINGLE)
|
||||
_flow_mock.reset_mock()
|
||||
mock_taskflow_load.reset_mock()
|
||||
mock_eng = mock.Mock()
|
||||
mock_eng_post = mock.Mock()
|
||||
mock_taskflow_load.side_effect = [mock_eng, mock_eng_post]
|
||||
_post_flow = mock.MagicMock()
|
||||
mock_get_get_post_lb_amp_association_flow.return_value = _post_flow
|
||||
store = {constants.LOADBALANCER_ID: LB_ID,
|
||||
'update_dict': {'topology': 'SINGLE'}}
|
||||
|
||||
store = {constants.LOADBALANCER_ID: LB_ID}
|
||||
cw = controller_worker.ControllerWorker()
|
||||
cw.create_load_balancer(LB_ID)
|
||||
|
||||
calls = [mock.call(_flow_mock, store=store),
|
||||
mock.call(_post_flow, store=store)]
|
||||
(base_taskflow.BaseTaskFlowEngine._taskflow_load.
|
||||
assert_called_once_with(_flow_mock, store=store))
|
||||
assert_has_calls(calls, any_order=True))
|
||||
mock_eng.run.assert_any_call()
|
||||
mock_eng_post.run.assert_any_call()
|
||||
|
||||
_flow_mock.run.assert_called_once_with()
|
||||
|
||||
self.assertFalse(mock_get_create_amp_for_lb_flow.called)
|
||||
|
||||
# Test code path with no existing READY amphora
|
||||
# Test the code path with an ACTIVE_STANDBY topology
|
||||
CONF.set_override(group='controller_worker',
|
||||
name='loadbalancer_topology',
|
||||
override=constants.TOPOLOGY_ACTIVE_STANDBY)
|
||||
|
||||
_flow_mock.reset_mock()
|
||||
mock_get_create_lb_flow.reset_mock()
|
||||
mock_taskflow_load.reset_mock()
|
||||
|
||||
mock_eng = mock.Mock()
|
||||
mock_taskflow_load.return_value = mock_eng
|
||||
mock_eng.run.side_effect = [exceptions.NoReadyAmphoraeException, None]
|
||||
mock_eng_post = mock.Mock()
|
||||
mock_taskflow_load.side_effect = [mock_eng, mock_eng_post]
|
||||
_post_flow = mock.MagicMock()
|
||||
mock_get_get_post_lb_amp_association_flow.return_value = _post_flow
|
||||
store = {constants.LOADBALANCER_ID: LB_ID,
|
||||
'update_dict': {'topology': 'ACTIVE_STANDBY'}}
|
||||
|
||||
cw = controller_worker.ControllerWorker()
|
||||
cw.create_load_balancer(LB_ID)
|
||||
|
||||
# mock is showing odd calls, even persisting through a reset
|
||||
# mock_taskflow_load.assert_has_calls([
|
||||
# mock.call(_flow_mock, store=store),
|
||||
# mock.call('TEST2', store=store),
|
||||
# ], anyorder=False)
|
||||
|
||||
calls = [mock.call(_flow_mock, store=store),
|
||||
mock.call(_post_flow, store=store)]
|
||||
(base_taskflow.BaseTaskFlowEngine._taskflow_load.
|
||||
assert_has_calls(calls, any_order=True))
|
||||
mock_eng.run.assert_any_call()
|
||||
|
||||
self.assertEqual(2, mock_eng.run.call_count)
|
||||
|
||||
mock_eng.reset()
|
||||
mock_eng.run = mock.MagicMock(
|
||||
side_effect=[exceptions.NoReadyAmphoraeException,
|
||||
exceptions.ComputeBuildException])
|
||||
|
||||
self.assertRaises(exceptions.NoSuitableAmphoraException,
|
||||
cw.create_load_balancer,
|
||||
LB_ID)
|
||||
mock_eng_post.run.assert_any_call()
|
||||
|
||||
@mock.patch('octavia.controller.worker.flows.load_balancer_flows.'
|
||||
'LoadBalancerFlows.get_delete_load_balancer_flow',
|
||||
|
||||
@@ -448,8 +448,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
server=n_constants.MOCK_COMPUTE_ID, port_id=port2.get('id'))
|
||||
|
||||
def test_update_vip(self):
|
||||
listeners = [data_models.Listener(protocol_port=80),
|
||||
data_models.Listener(protocol_port=443)]
|
||||
listeners = [data_models.Listener(protocol_port=80, peer_port=1024),
|
||||
data_models.Listener(protocol_port=443, peer_port=1025)]
|
||||
lb = data_models.LoadBalancer(id='1', listeners=listeners)
|
||||
list_sec_grps = self.driver.neutron_client.list_security_groups
|
||||
list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
|
||||
@@ -465,7 +465,25 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
create_rule = self.driver.neutron_client.create_security_group_rule
|
||||
self.driver.update_vip(lb)
|
||||
delete_rule.assert_called_once_with('rule-22')
|
||||
expected_create_rule = {
|
||||
expected_create_rule_1 = {
|
||||
'security_group_rule': {
|
||||
'security_group_id': 'secgrp-1',
|
||||
'direction': 'ingress',
|
||||
'protocol': 'TCP',
|
||||
'port_range_min': 1024,
|
||||
'port_range_max': 1024
|
||||
}
|
||||
}
|
||||
expected_create_rule_2 = {
|
||||
'security_group_rule': {
|
||||
'security_group_id': 'secgrp-1',
|
||||
'direction': 'ingress',
|
||||
'protocol': 'TCP',
|
||||
'port_range_min': 1025,
|
||||
'port_range_max': 1025
|
||||
}
|
||||
}
|
||||
expected_create_rule_3 = {
|
||||
'security_group_rule': {
|
||||
'security_group_id': 'secgrp-1',
|
||||
'direction': 'ingress',
|
||||
@@ -474,7 +492,9 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
'port_range_max': 443
|
||||
}
|
||||
}
|
||||
create_rule.assert_called_once_with(expected_create_rule)
|
||||
create_rule.assert_has_calls([mock.call(expected_create_rule_1),
|
||||
mock.call(expected_create_rule_2),
|
||||
mock.call(expected_create_rule_3)])
|
||||
|
||||
def test_update_vip_when_listener_deleted(self):
|
||||
listeners = [data_models.Listener(protocol_port=80),
|
||||
@@ -496,7 +516,7 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
create_rule = self.driver.neutron_client.create_security_group_rule
|
||||
self.driver.update_vip(lb)
|
||||
delete_rule.assert_called_once_with('rule-22')
|
||||
self.assertFalse(create_rule.called)
|
||||
self.assertTrue(create_rule.called)
|
||||
|
||||
def test_update_vip_when_no_listeners(self):
|
||||
listeners = []
|
||||
@@ -512,10 +532,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
list_rules = self.driver.neutron_client.list_security_group_rules
|
||||
list_rules.return_value = fake_rules
|
||||
delete_rule = self.driver.neutron_client.delete_security_group_rule
|
||||
create_rule = self.driver.neutron_client.create_security_group_rule
|
||||
self.driver.update_vip(lb)
|
||||
delete_rule.assert_called_once_with('ssh-rule')
|
||||
self.assertFalse(create_rule.called)
|
||||
|
||||
def test_failover_preparation(self):
|
||||
ports = {"ports": [
|
||||
|
||||
@@ -120,4 +120,4 @@ class TestNoopNetworkDriver(base.TestCase):
|
||||
self.assertEqual(
|
||||
(self.port_id, 'get_port'),
|
||||
self.driver.driver.networkconfigconfig[self.port_id]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ console_scripts =
|
||||
octavia-health-manager = octavia.cmd.health_manager:main
|
||||
octavia-housekeeping = octavia.cmd.house_keeping:main
|
||||
amphora-agent = octavia.cmd.agent:main
|
||||
haproxy-vrrp-check = octavia.cmd.haproxy_vrrp_check:main
|
||||
octavia.api.handlers =
|
||||
simulated_handler = octavia.api.v1.handlers.controller_simulator.handler:SimulatedControllerHandler
|
||||
queue_producer = octavia.api.v1.handlers.queue.producer:ProducerHandler
|
||||
|
||||
Reference in New Issue
Block a user