Merge "Amphora Flows and Drivers for Active Standby"

This commit is contained in:
Jenkins
2015-12-04 16:26:08 +00:00
committed by Gerrit Code Review
66 changed files with 2680 additions and 257 deletions

View File

@@ -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',
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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', ...]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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] = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,4 +120,4 @@ class TestNoopNetworkDriver(base.TestCase):
self.assertEqual(
(self.port_id, 'get_port'),
self.driver.driver.networkconfigconfig[self.port_id]
)
)

View File

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