UDP for [2]

These files will split with the current Octavia repo, before other parts
are ok.

Patch List:

[1] Finish keepalived LVS jinja template for UDP support
[2] Extend the ability of amp agent for upload/refresh the keepalived
process
[3] Extend the db model and db table with necessary fields for met the new
udp backend
[4] Add logic/workflow elements process in UDP cases
[5] Extend the existing API to access udp parameters in Listener API
[6] Extend the existing pool API to access the new option in
session_persistence fields

Change-Id: Ib4924e602d450b1feadb29e830d715ae77f5bbfe
This commit is contained in:
ZhaoBo 2017-12-21 10:52:46 +08:00
parent 8004fe23bd
commit a890f2ba35
39 changed files with 2959 additions and 148 deletions

View File

@ -310,6 +310,10 @@
# agent_server_network_file =
# agent_request_read_timeout = 120
# Amphora default UDP driver is keepalived_lvs
#
# amphora_udp_driver = keepalived_lvs
[keepalived_vrrp]
# Amphora Role/Priority advertisement interval in seconds
# vrrp_advert_int = 1

View File

@ -59,4 +59,5 @@ class AgentJinjaTemplater(object):
'heartbeat_key': CONF.health_manager.heartbeat_key,
'use_upstart': CONF.haproxy_amphora.use_upstart,
'respawn_count': CONF.haproxy_amphora.respawn_count,
'respawn_interval': CONF.haproxy_amphora.respawn_interval})
'respawn_interval': CONF.haproxy_amphora.respawn_interval,
'amphora_udp_driver': CONF.amphora_agent.amphora_udp_driver})

View File

@ -32,48 +32,68 @@ class AmphoraInfo(object):
def __init__(self, osutils):
self._osutils = osutils
def compile_amphora_info(self):
return webob.Response(
json={'hostname': socket.gethostname(),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION})
def compile_amphora_info(self, extend_udp_driver=None):
extend_body = {}
if extend_udp_driver:
extend_body = self._get_extend_body_from_udp_driver(
extend_udp_driver)
body = {'hostname': socket.gethostname(),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION}
if extend_body:
body.update(extend_body)
return webob.Response(json=body)
def compile_amphora_details(self):
listener_list = util.get_listeners()
def compile_amphora_details(self, extend_udp_driver=None):
haproxy_listener_list = util.get_listeners()
extend_body = {}
udp_listener_list = []
if extend_udp_driver:
udp_listener_list = util.get_udp_listeners()
extend_data = self._get_extend_body_from_udp_driver(
extend_udp_driver)
udp_count = self._count_udp_listener_processes(extend_udp_driver,
udp_listener_list)
extend_body['udp_listener_process_count'] = udp_count
extend_body.update(extend_data)
meminfo = self._get_meminfo()
cpu = self._cpu()
st = os.statvfs('/')
return webob.Response(
json={'hostname': socket.gethostname(),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION,
'networks': self._get_networks(),
'active': True,
'haproxy_count':
self._count_haproxy_processes(listener_list),
'cpu': {
'total': cpu['total'],
'user': cpu['user'],
'system': cpu['system'],
'soft_irq': cpu['softirq'], },
'memory': {
'total': meminfo['MemTotal'],
'free': meminfo['MemFree'],
'buffers': meminfo['Buffers'],
'cached': meminfo['Cached'],
'swap_used': meminfo['SwapCached'],
'shared': meminfo['Shmem'],
'slab': meminfo['Slab'], },
'disk': {
'used': (st.f_blocks - st.f_bfree) * st.f_frsize,
'available': st.f_bavail * st.f_frsize},
'load': self._load(),
'topology': consts.TOPOLOGY_SINGLE,
'topology_status': consts.TOPOLOGY_STATUS_OK,
'listeners': listener_list,
'packages': {}})
body = {'hostname': socket.gethostname(),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION,
'networks': self._get_networks(),
'active': True,
'haproxy_count':
self._count_haproxy_processes(haproxy_listener_list),
'cpu': {
'total': cpu['total'],
'user': cpu['user'],
'system': cpu['system'],
'soft_irq': cpu['softirq'], },
'memory': {
'total': meminfo['MemTotal'],
'free': meminfo['MemFree'],
'buffers': meminfo['Buffers'],
'cached': meminfo['Cached'],
'swap_used': meminfo['SwapCached'],
'shared': meminfo['Shmem'],
'slab': meminfo['Slab'], },
'disk': {
'used': (st.f_blocks - st.f_bfree) * st.f_frsize,
'available': st.f_bavail * st.f_frsize},
'load': self._load(),
'topology': consts.TOPOLOGY_SINGLE,
'topology_status': consts.TOPOLOGY_STATUS_OK,
'listeners': list(
set(haproxy_listener_list + udp_listener_list))
if udp_listener_list else haproxy_listener_list,
'packages': {}}
if extend_body:
body.update(extend_body)
return webob.Response(json=body)
def _get_version_of_installed_package(self, name):
@ -89,6 +109,22 @@ class AmphoraInfo(object):
num += 1
return num
def _count_udp_listener_processes(self, udp_driver, listener_list):
num = 0
for listener_id in listener_list:
if udp_driver.is_listener_running(listener_id):
# optional check if it's still running
num += 1
return num
def _get_extend_body_from_udp_driver(self, extend_udp_driver):
extend_info = extend_udp_driver.get_subscribed_amp_compile_info()
extend_data = {}
for extend in extend_info:
package_version = self._get_version_of_installed_package(extend)
extend_data['%s_version' % extend] = package_version
return extend_data
def _get_meminfo(self):
re_parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB')
result = dict()

View File

@ -0,0 +1,352 @@
# Copyright 2011-2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import re
import shutil
import stat
import subprocess
import flask
import jinja2
from oslo_log import log as logging
import webob
from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import listener
from octavia.amphorae.backends.agent.api_server import udp_listener_base
from octavia.amphorae.backends.agent.api_server import util
from octavia.amphorae.backends.utils import keepalivedlvs_query
from octavia.common import constants as consts
BUFFER = 100
CHECK_SCRIPT_NAME = 'udp_check.sh'
KEEPALIVED_CHECK_SCRIPT_NAME = 'lvs_udp_check.sh'
LOG = logging.getLogger(__name__)
j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader(
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
UPSTART_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_UPSTART)
SYSVINIT_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_SYSVINIT)
SYSTEMD_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_SYSTEMD)
check_script_file_template = j2_env.get_template(
consts.KEEPALIVED_CHECK_SCRIPT)
class KeepalivedLvs(udp_listener_base.UdpListenerApiServerBase):
_SUBSCRIBED_AMP_COMPILE = ['keepalived', 'ipvsadm']
def upload_udp_listener_config(self, listener_id):
stream = listener.Wrapped(flask.request.stream)
NEED_CHECK = True
if not os.path.exists(util.keepalived_lvs_dir()):
os.makedirs(util.keepalived_lvs_dir())
if not os.path.exists(util.keepalived_backend_check_script_dir()):
current_file_dir, _ = os.path.split(os.path.abspath(__file__))
try:
script_dir = os.path.join(os.path.abspath(
os.path.join(current_file_dir, '../..')), 'utils')
assert True is os.path.exists(script_dir)
assert True is os.path.exists(os.path.join(
script_dir, CHECK_SCRIPT_NAME))
except Exception:
raise exceptions.Conflict(
description='%(file_name)s not Found for '
'UDP Listener %(listener_id)s' %
{'file_name': CHECK_SCRIPT_NAME,
'listener_id': listener_id})
os.makedirs(util.keepalived_backend_check_script_dir())
shutil.copy2(os.path.join(script_dir, CHECK_SCRIPT_NAME),
util.keepalived_backend_check_script_path())
os.chmod(util.keepalived_backend_check_script_path(), stat.S_IEXEC)
# Based on current topology setting, only the amphora instances in
# Active-Standby topology will create the directory below. So for
# Single topology, it should not create the directory and the check
# scripts for status change.
if not os.path.exists(util.keepalived_check_scripts_dir()):
NEED_CHECK = False
conf_file = util.keepalived_lvs_cfg_path(listener_id)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# mode 00644
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
with os.fdopen(os.open(conf_file, flags, mode), 'wb') as f:
b = stream.read(BUFFER)
while b:
f.write(b)
b = stream.read(BUFFER)
init_system = util.get_os_init_system()
file_path = util.keepalived_lvs_init_path(init_system, listener_id)
if init_system == consts.INIT_SYSTEMD:
template = SYSTEMD_TEMPLATE
init_enable_cmd = ("systemctl enable "
"octavia-keepalivedlvs-%s"
% str(listener_id))
elif init_system == consts.INIT_UPSTART:
template = UPSTART_TEMPLATE
elif init_system == consts.INIT_SYSVINIT:
template = SYSVINIT_TEMPLATE
init_enable_cmd = "insserv {file}".format(file=file_path)
else:
raise util.UnknownInitError()
if init_system == consts.INIT_SYSTEMD:
# mode 00644
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
else:
# mode 00755
mode = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
keepalived_pid, vrrp_pid, check_pid = util.keepalived_lvs_pids_path(
listener_id)
if not os.path.exists(file_path):
with os.fdopen(os.open(file_path, flags, mode), 'w') as text_file:
text = template.render(
keepalived_pid=keepalived_pid,
vrrp_pid=vrrp_pid,
check_pid=check_pid,
keepalived_cmd=consts.KEEPALIVED_CMD,
keepalived_cfg=util.keepalived_lvs_cfg_path(listener_id),
amphora_nsname=consts.AMPHORA_NAMESPACE
)
text_file.write(text)
# Make sure the new service is enabled on boot
if init_system != consts.INIT_UPSTART:
try:
subprocess.check_output(init_enable_cmd.split(),
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.debug('Failed to enable '
'octavia-keepalivedlvs service: '
'%(err)s', {'err': e})
return webob.Response(json=dict(
message="Error enabling "
"octavia-keepalivedlvs service",
details=e.output), status=500)
if NEED_CHECK:
# inject the check script for keepalived process
script_path = os.path.join(util.keepalived_check_scripts_dir(),
KEEPALIVED_CHECK_SCRIPT_NAME)
if not os.path.exists(script_path):
with os.fdopen(os.open(script_path, flags, stat.S_IEXEC),
'w') as script_file:
text = check_script_file_template.render(
consts=consts,
init_system=init_system,
keepalived_lvs_pid_dir=util.keepalived_lvs_dir()
)
script_file.write(text)
res = webob.Response(json={'message': 'OK'}, status=200)
res.headers['ETag'] = stream.get_md5()
return res
def _check_udp_listener_exists(self, listener_id):
if not os.path.exists(util.keepalived_lvs_cfg_path(listener_id)):
raise exceptions.HTTPException(
response=webob.Response(json=dict(
message='UDP Listener Not Found',
details="No UDP listener with UUID: {0}".format(
listener_id)), status=404))
def get_udp_listener_config(self, listener_id):
"""Gets the keepalivedlvs config
:param listener_id: the id of the listener
"""
self._check_udp_listener_exists(listener_id)
with open(util.keepalived_lvs_cfg_path(listener_id), 'r') as file:
cfg = file.read()
resp = webob.Response(cfg, content_type='text/plain')
return resp
def manage_udp_listener(self, listener_id, action):
action = action.lower()
if action not in [consts.AMP_ACTION_START,
consts.AMP_ACTION_STOP,
consts.AMP_ACTION_RELOAD]:
return webob.Response(json=dict(
message='Invalid Request',
details="Unknown action: {0}".format(action)), status=400)
self._check_udp_listener_exists(listener_id)
if action == consts.AMP_ACTION_RELOAD:
if consts.OFFLINE == self._check_udp_listener_status(listener_id):
action = consts.AMP_ACTION_START
cmd = ("/usr/sbin/service "
"octavia-keepalivedlvs-{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 %s keepalivedlvs listener %s',
listener_id + ' : ' + action, e)
return webob.Response(json=dict(
message=("Failed to {0} keepalivedlvs listener {1}"
.format(action, listener_id)),
details=e.output), status=500)
return webob.Response(
json=dict(message='OK',
details='keepalivedlvs listener {listener_id}'
'{action}ed'.format(listener_id=listener_id,
action=action)),
status=202)
def _check_udp_listener_status(self, listener_id):
if os.path.exists(util.keepalived_lvs_pids_path(listener_id)[0]):
if os.path.exists(os.path.join(
'/proc', util.get_keepalivedlvs_pid(listener_id))):
# Check if the listener is disabled
with open(util.keepalived_lvs_cfg_path(listener_id),
'r') as file:
cfg = file.read()
m = re.search('virtual_server', cfg)
if m:
return consts.ACTIVE
return consts.OFFLINE
return consts.ERROR
return consts.OFFLINE
def get_all_udp_listeners_status(self):
"""Gets the status of all UDP listeners
This method will not consult the stats socket
so a listener might show as ACTIVE but still be
in ERROR
"""
listeners = list()
for udp_listener in util.get_udp_listeners():
status = self._check_udp_listener_status(udp_listener)
listeners.append({
'status': status,
'uuid': udp_listener,
'type': 'lvs',
})
return listeners
def get_udp_listener_status(self, listener_id):
"""Gets the status of a UDP listener
This method will consult the stats socket
so calling this method will interfere with
the health daemon with the risk of the amphora
shut down
:param listener_id: The id of the listener
"""
self._check_udp_listener_exists(listener_id)
status = self._check_udp_listener_status(listener_id)
if status != consts.ACTIVE:
stats = dict(
status=status,
uuid=listener_id,
type=''
)
return webob.Response(json=stats)
stats = dict(
status=status,
uuid=listener_id,
type='lvs'
)
try:
pool = keepalivedlvs_query.get_udp_listener_pool_status(
listener_id)
except subprocess.CalledProcessError as e:
return webob.Response(json=dict(
message="Error get kernel lvs status for udp listener",
details=e.output), status=500)
stats['pools'] = [pool]
return webob.Response(json=stats)
def delete_udp_listener(self, listener_id):
try:
self._check_udp_listener_exists(listener_id)
except exceptions.HTTPException:
return webob.Response(json={'message': 'OK'})
# check if that keepalived is still running and if stop it
keepalived_pid, vrrp_pid, check_pid = util.keepalived_lvs_pids_path(
listener_id)
if os.path.exists(keepalived_pid) and os.path.exists(
os.path.join('/proc',
util.get_keepalivedlvs_pid(listener_id))):
cmd = ("/usr/sbin/service "
"octavia-keepalivedlvs-{0} stop".format(listener_id))
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error("Failed to stop keepalivedlvs service: %s", e)
return webob.Response(json=dict(
message="Error stopping keepalivedlvs",
details=e.output), status=500)
# Since the lvs check script based on the keepalived pid file for
# checking whether it is alived. So here, we had stop the keepalived
# process by the previous step, must make sure the pid files are not
# exist.
if (os.path.exists(keepalived_pid) or
os.path.exists(vrrp_pid) or os.path.exists(check_pid)):
for pid in [keepalived_pid, vrrp_pid, check_pid]:
os.remove(pid)
# disable the service
init_system = util.get_os_init_system()
init_path = util.keepalived_lvs_init_path(init_system, listener_id)
if init_system == consts.INIT_SYSTEMD:
init_disable_cmd = (
"systemctl disable octavia-keepalivedlvs-"
"{list}".format(list=listener_id))
elif init_system == consts.INIT_SYSVINIT:
init_disable_cmd = "insserv -r {file}".format(file=init_path)
elif init_system != consts.INIT_UPSTART:
raise util.UnknownInitError()
if init_system != consts.INIT_UPSTART:
try:
subprocess.check_output(init_disable_cmd.split(),
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error("Failed to disable "
"octavia-keepalivedlvs-%(list)s service: "
"%(err)s", {'list': listener_id, 'err': e})
return webob.Response(json=dict(
message=(
"Error disabling octavia-keepalivedlvs-"
"{0} service".format(listener_id)),
details=e.output), status=500)
# delete init script ,config file and log file for that listener
if os.path.exists(init_path):
os.remove(init_path)
if os.path.exists(util.keepalived_lvs_cfg_path(listener_id)):
os.remove(util.keepalived_lvs_cfg_path(listener_id))
return webob.Response(json={'message': 'OK'})

View File

@ -362,7 +362,7 @@ class Listener(object):
return webob.Response(json={'message': 'OK'})
def get_all_listeners_status(self):
def get_all_listeners_status(self, other_listeners=None):
"""Gets the status of all listeners
This method will not consult the stats socket
@ -386,6 +386,8 @@ class Listener(object):
'type': listener_type,
})
if other_listeners:
listeners = listeners + other_listeners
return webob.Response(json=listeners, content_type='application/json')
def get_listener_status(self, listener_id):

View File

@ -315,6 +315,10 @@ class RH(BaseOS):
ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2'
ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2'
RULE_ETH_X_CONF = 'rh_rule_ethX.conf.j2'
# The reason of make them as jinja templates is the current scripts force
# to add the iptables, so leave it now for future extending if possible.
ETH_IFUP_LOCAL_SCRIPT = 'rh_plug_port_eth_ifup_local.conf.j2'
ETH_IFDOWN_LOCAL_SCRIPT = 'rh_plug_port_eth_ifdown_local.conf.j2'
@classmethod
def is_os_name(cls, os_name):
@ -411,6 +415,8 @@ class RH(BaseOS):
route_rules_interface_file_path, primary_interface,
render_host_routes, template_rules, gateway, vip, netmask)
self._write_ifup_ifdown_local_scripts_if_possible()
def write_static_routes_interface_file(self, interface_file_path,
interface, host_routes,
template_routes, gateway,
@ -460,6 +466,7 @@ class RH(BaseOS):
self.write_static_routes_interface_file(
routes_interface_file_path, netns_interface,
host_routes, template_routes, None, None, None)
self._write_ifup_ifdown_local_scripts_if_possible()
def bring_interfaces_up(self, ip, primary_interface, secondary_interface):
if ip.version == 4:
@ -473,6 +480,43 @@ class RH(BaseOS):
def has_ifup_all(self):
return False
def _write_ifup_ifdown_local_scripts_if_possible(self):
if self._check_ifup_ifdown_local_scripts_exists():
template_ifup_local = j2_env.get_template(
self.ETH_IFUP_LOCAL_SCRIPT)
self.write_port_interface_if_local_scripts(template_ifup_local)
template_ifdown_local = j2_env.get_template(
self.ETH_IFDOWN_LOCAL_SCRIPT)
self.write_port_interface_if_local_scripts(template_ifdown_local,
ifup=False)
def _check_ifup_ifdown_local_scripts_exists(self):
file_names = ['ifup-local', 'ifdown-local']
target_dir = '/sbin/'
res = []
for file_name in file_names:
if os.path.exists(os.path.join(target_dir, file_name)):
res.append(True)
else:
res.append(False)
# This means we only add the scripts when both of them are non-exists
return not any(res)
def write_port_interface_if_local_scripts(
self, template_script, ifup=True):
file_name = 'ifup' + '-local'
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
if not ifup:
file_name = 'ifdown' + '-local'
with os.fdopen(
os.open(os.path.join(
'/sbin/', file_name), flags, mode), 'w') as text_file:
text = template_script.render()
text_file.write(text)
os.chmod(os.path.join('/sbin/', file_name), stat.S_IEXEC)
class CentOS(RH):

View File

@ -127,10 +127,26 @@ class Plug(object):
sysctl = pyroute2.NSPopen(consts.AMPHORA_NAMESPACE,
[consts.SYSCTL_CMD, '--system'],
stdout=subprocess.PIPE)
sysctl.communicate()
sysctl.wait()
sysctl.release()
cmd_list = [['modprobe', 'ip_vs'],
[consts.SYSCTL_CMD, '-w', 'net.ipv4.vs.conntrack=1']]
if ip.version == 4:
# For lvs function, enable ip_vs kernel module, enable ip_forward
# conntrack in amphora network namespace.
cmd_list.append([consts.SYSCTL_CMD, '-w', 'net.ipv4.ip_forward=1'])
elif ip.version == 6:
cmd_list.append([consts.SYSCTL_CMD, '-w',
'net.ipv6.conf.all.forwarding=1'])
for cmd in cmd_list:
ns_exec = pyroute2.NSPopen(consts.AMPHORA_NAMESPACE, cmd,
stdout=subprocess.PIPE)
ns_exec.wait()
ns_exec.release()
with pyroute2.IPRoute() as ipr:
# Move the interfaces into the namespace
idx = ipr.link_lookup(ifname=default_netns_interface)[0]

View File

@ -25,6 +25,7 @@ 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 osutils
from octavia.amphorae.backends.agent.api_server import plug
from octavia.amphorae.backends.agent.api_server import udp_listener_base
PATH_PREFIX = '/' + api_server.VERSION
@ -42,12 +43,25 @@ def register_app_error_handler(app):
app.register_error_handler(code, make_json_error)
def check_and_return_request_listener_protocol(request):
try:
protocol_dict = request.get_json()
assert type(protocol_dict) is dict
assert 'protocol' in protocol_dict
except Exception:
raise exceptions.BadRequest(
description='Invalid protocol information for Listener')
return protocol_dict['protocol']
class Server(object):
def __init__(self):
self.app = flask.Flask(__name__)
self._osutils = osutils.BaseOS.get_os_util()
self._keepalived = keepalived.Keepalived()
self._listener = listener.Listener()
self._udp_listener = (udp_listener_base.UdpListenerApiServerBase.
get_server_driver())
self._plug = plug.Plug(self._osutils)
self._amphora_info = amphora_info.AmphoraInfo(self._osutils)
@ -57,10 +71,19 @@ class Server(object):
'/listeners/<amphora_id>/<listener_id>/haproxy',
view_func=self.upload_haproxy_config,
methods=['PUT'])
self.app.add_url_rule(rule=PATH_PREFIX +
'/listeners/<amphora_id>/<listener_id>'
'/udp_listener',
view_func=self.upload_udp_listener_config,
methods=['PUT'])
self.app.add_url_rule(rule=PATH_PREFIX +
'/listeners/<listener_id>/haproxy',
view_func=self.get_haproxy_config,
methods=['GET'])
self.app.add_url_rule(rule=PATH_PREFIX +
'/listeners/<listener_id>/udp_listener',
view_func=self.get_udp_listener_config,
methods=['GET'])
self.app.add_url_rule(rule=PATH_PREFIX +
'/listeners/<listener_id>/<action>',
view_func=self.start_stop_listener,
@ -113,25 +136,48 @@ class Server(object):
def upload_haproxy_config(self, amphora_id, listener_id):
return self._listener.upload_haproxy_config(amphora_id, listener_id)
def upload_udp_listener_config(self, amphora_id, listener_id):
return self._udp_listener.upload_udp_listener_config(listener_id)
def get_haproxy_config(self, listener_id):
return self._listener.get_haproxy_config(listener_id)
def get_udp_listener_config(self, listener_id):
return self._udp_listener.get_udp_listener_config(listener_id)
def start_stop_listener(self, listener_id, action):
protocol = check_and_return_request_listener_protocol(
flask.request)
if protocol == 'UDP':
return self._udp_listener.manage_udp_listener(
listener_id, action)
return self._listener.start_stop_listener(listener_id, action)
def delete_listener(self, listener_id):
protocol = check_and_return_request_listener_protocol(
flask.request)
if protocol == 'UDP':
return self._udp_listener.delete_udp_listener(listener_id)
return self._listener.delete_listener(listener_id)
def get_details(self):
return self._amphora_info.compile_amphora_details()
return self._amphora_info.compile_amphora_details(
extend_udp_driver=self._udp_listener)
def get_info(self):
return self._amphora_info.compile_amphora_info()
return self._amphora_info.compile_amphora_info(
extend_udp_driver=self._udp_listener)
def get_all_listeners_status(self):
return self._listener.get_all_listeners_status()
udp_listeners = self._udp_listener.get_all_udp_listeners_status()
return self._listener.get_all_listeners_status(
other_listeners=udp_listeners)
def get_listener_status(self, listener_id):
protocol = check_and_return_request_listener_protocol(
flask.request)
if protocol == 'UDP':
return self._udp_listener.get_udp_listener_status(listener_id)
return self._listener.get_listener_status(listener_id)
def upload_certificate(self, listener_id, filename):

View File

@ -10,6 +10,12 @@ RemainAfterExit=yes
ExecStart=-/sbin/ip netns add {{ amphora_nsname }}
# Load the system sysctl into the new namespace
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl --system
# Enable kernel module ip_vs for lvs function in amphora network namespace
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} modprobe ip_vs
# Enable ip_forward and conntrack kernel configuration
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv4.ip_forward=1
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv4.vs.conntrack=1
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv6.conf.all.forwarding=1
# We need the plugged_interfaces file sorted to join the host interfaces
ExecStart=-/bin/sh -c '/usr/bin/sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted'
# Assign the interfaces into the namespace with the appropriate name

View File

@ -8,7 +8,11 @@ Wants=network-online.target
SELinuxContext=system_u:system_r:keepalived_t:s0
Type=forking
KillMode=process
{% if vrrp_pid and check_pid %}
ExecStart=/sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }}
{% else %}
ExecStart=/sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }}
{% endif %}
ExecReload=/bin/kill -HUP $MAINPID
PIDFile={{ keepalived_pid }}

View File

@ -18,7 +18,11 @@ DAEMON="ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }}"
NAME=octavia-keepalived
DESC=octavia-keepalived
TMPFILES="/tmp/.vrrp /tmp/.healthcheckers"
{% if vrrp_pid and check_pid %}
DAEMON_ARGS="-D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }}"
{% else %}
DAEMON_ARGS="-D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }}"
{% endif %}
#includes lsb functions
. /lib/lsb/init-functions

View File

@ -22,4 +22,8 @@ stop on runlevel [!2345]
respawn
{% if vrrp_pid and check_pid %}
exec /sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -n -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }}
{% else %}
exec /sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -n -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }}
{% endif %}

View File

@ -0,0 +1,21 @@
#!/bin/bash
# Don't try to run the directory when it is empty
shopt -s nullglob
status=0
for file in {{ keepalived_lvs_pid_dir }}/*
do
file_ext=${file#*.}
case $file_ext in
pid) echo "Check keepalived pid file: " $file;;
*) continue;;
esac
{% if init_system == consts.INIT_SYSTEMD %}
systemctl status $(basename $file .pid) > /dev/null
{% elif init_system in (consts.INIT_UPSTART, consts.INIT_SYSVINIT) %}
kill -0 `cat $file`
{% endif %}
status=$(( $status + $? ))
done
exit $status

View File

@ -27,6 +27,8 @@ mtu {{ mtu }}
up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endfor %}
post-up /sbin/ip{{ '6' if ipv6 }}tables -t nat -A POSTROUTING -p udp -o {{ interface }} -j MASQUERADE
post-down /sbin/ip{{ '6' if ipv6 }}tables -t nat -D POSTROUTING -p udp -o {{ interface }} -j MASQUERADE
{%- else %}
iface {{ interface }} inet dhcp
auto {{ interface }}:0

View File

@ -53,3 +53,5 @@ post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ hr.network }} via {{ hr.g
{%- endfor %}
post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}rule add from {{ vip }}/32 table 1 priority 100
post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}rule del from {{ vip }}/32 table 1 priority 100
post-up /sbin/ip{{ '6' if vip_ipv6 }}tables -t nat -A POSTROUTING -p udp -o {{ interface }} -j MASQUERADE
post-down /sbin/ip{{ '6' if vip_ipv6 }}tables -t nat -D POSTROUTING -p udp -o {{ interface }} -j MASQUERADE

View File

@ -0,0 +1,19 @@
{# 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.
#}
# Generated by Octavia agent
#!/bin/bash
if [[ "$1" != "lo" ]]
then
/sbin/iptables -t nat -D POSTROUTING -o $1 -p udp -j MASQUERADE
/sbin/ip6tables -t nat -D POSTROUTING -o $1 -p udp -j MASQUERADE
fi

View File

@ -0,0 +1,19 @@
{# 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.
#}
# Generated by Octavia agent
#!/bin/bash
if [[ "$1" != "lo" ]]
then
/sbin/iptables -t nat -A POSTROUTING -o $1 -p udp -j MASQUERADE
/sbin/ip6tables -t nat -A POSTROUTING -o $1 -p udp -j MASQUERADE
fi

View File

@ -0,0 +1,123 @@
# Copyright 2018 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
import six
from oslo_config import cfg
from stevedore import driver as stevedore_driver
CONF = cfg.CONF
UDP_SERVER_NAMESPACE = 'octavia.amphora.udp_api_server'
@six.add_metaclass(abc.ABCMeta)
class UdpListenerApiServerBase(object):
"""Base UDP Listener Server API
"""
_SUBSCRIBED_AMP_COMPILE = []
SERVER_INSTANCE = None
@classmethod
def get_server_driver(cls):
if not cls.SERVER_INSTANCE:
cls.SERVER_INSTANCE = stevedore_driver.DriverManager(
namespace=UDP_SERVER_NAMESPACE,
name=CONF.amphora_agent.amphora_udp_driver,
invoke_on_load=True,
).driver
return cls.SERVER_INSTANCE
def get_subscribed_amp_compile_info(self):
return self._SUBSCRIBED_AMP_COMPILE
@abc.abstractmethod
def upload_udp_listener_config(self, listener_id):
"""Upload the configuration for UDP.
:param listener_id: The id of a UDP Listener
:returns: HTTP response with status code.
:raises Exception: If any file / directory is not found or
fail to create.
"""
pass
@abc.abstractmethod
def get_udp_listener_config(self, listener_id):
"""Gets the UDP Listener configuration details
:param listener_id: the id of the UDP Listener
:returns: HTTP response with status code.
:raises Exception: If the listener is failed to find.
"""
pass
@abc.abstractmethod
def manage_udp_listener(self, listener_id, action):
"""Gets the UDP Listener configuration details
:param listener_id: the id of the UDP Listener
:param action: the operation type.
:returns: HTTP response with status code.
:raises Exception: If the listener is failed to find.
"""
pass
@abc.abstractmethod
def get_all_udp_listeners_status(self):
"""Gets the status of all UDP Listeners
This method will not consult the stats socket
so a listener might show as ACTIVE but still be
in ERROR
:returns: a list of UDP Listener status
:raises Exception: If the listener pid located directory is not exist
"""
pass
@abc.abstractmethod
def get_udp_listener_status(self, listener_id):
"""Gets the status of a UDP listener
:param listener_id: The id of the listener
:returns: HTTP response with status code.
:raises Exception: If the listener is failed to find.
"""
pass
@abc.abstractmethod
def delete_udp_listener(self, listener_id):
"""Delete a UDP Listener from a amphora
:param listener_id: The id of the listener
:returns: HTTP response with status code.
:raises Exception: If unsupport initial system of amphora.
"""
pass

View File

@ -14,6 +14,7 @@
import os
import re
import subprocess
from oslo_config import cfg
@ -41,6 +42,56 @@ def init_path(listener_id, init_system):
raise UnknownInitError()
def keepalived_lvs_dir():
return os.path.join(CONF.haproxy_amphora.base_path, 'lvs')
def keepalived_lvs_init_path(init_system, listener_id):
if init_system == consts.INIT_SYSTEMD:
return os.path.join(consts.SYSTEMD_DIR,
consts.KEEPALIVED_SYSTEMD_PREFIX %
str(listener_id))
elif init_system == consts.INIT_UPSTART:
return os.path.join(consts.UPSTART_DIR,
consts.KEEPALIVED_UPSTART_PREFIX %
str(listener_id))
elif init_system == consts.INIT_SYSVINIT:
return os.path.join(consts.SYSVINIT_DIR,
consts.KEEPALIVED_SYSVINIT_PREFIX %
str(listener_id))
else:
raise UnknownInitError()
def keepalived_backend_check_script_dir():
return os.path.join(CONF.haproxy_amphora.base_path, 'lvs/check/')
def keepalived_backend_check_script_path():
return os.path.join(keepalived_backend_check_script_dir(),
'udp_check.sh')
def keepalived_lvs_pids_path(listener_id):
pids_path = {}
for file_ext in ['pid', 'vrrp.pid', 'check.pid']:
pids_path[file_ext] = (
os.path.join(CONF.haproxy_amphora.base_path,
('lvs/octavia-keepalivedlvs-%s.%s') %
(str(listener_id), file_ext)))
return pids_path['pid'], pids_path['vrrp.pid'], pids_path['check.pid']
def keepalived_lvs_cfg_path(listener_id):
return os.path.join(CONF.haproxy_amphora.base_path,
('lvs/octavia-keepalivedlvs-%s.conf') %
str(listener_id))
def keepalived_lvs_iptables_dir():
return os.path.join(CONF.haproxy_amphora.base_path, 'lvs/iptables/')
def haproxy_dir(listener_id):
return os.path.join(CONF.haproxy_amphora.base_path, listener_id)
@ -58,6 +109,12 @@ def get_haproxy_pid(listener_id):
return f.readline().rstrip()
def get_keepalivedlvs_pid(listener_id):
pid_file, _, _ = keepalived_lvs_pids_path(listener_id)
with open(pid_file, 'r') as f:
return f.readline().rstrip()
def haproxy_sock_path(listener_id):
return os.path.join(CONF.haproxy_amphora.base_path, listener_id + '.sock')
@ -125,6 +182,24 @@ def is_listener_running(listener_id):
os.path.join('/proc', get_haproxy_pid(listener_id)))
def get_udp_listeners():
result = []
if os.path.exists(keepalived_lvs_dir()):
for f in os.listdir(keepalived_lvs_dir()):
if f.endswith('.conf'):
prefix = f.split('.')[0]
if re.search("octavia-keepalivedlvs-", prefix):
result.append(f.split(
'octavia-keepalivedlvs-')[1].split('.')[0])
return result
def is_udp_listener_running(listener_id):
pid_file, _, _ = keepalived_lvs_pids_path(listener_id)
return os.path.exists(pid_file) and os.path.exists(
os.path.join('/proc', get_keepalivedlvs_pid(listener_id)))
def get_os_init_system():
if os.path.exists(consts.INIT_PROC_COMM_PATH):
with open(consts.INIT_PROC_COMM_PATH, 'r') as init_comm:

View File

@ -42,3 +42,4 @@ agent_server_network_file = {{ agent_server_network_file }}
{% endif -%}
agent_request_read_timeout = {{ agent_request_read_timeout }}
amphora_id = {{ amphora_id }}
amphora_udp_driver = {{ amphora_udp_driver }}

View File

@ -25,6 +25,8 @@ import six
from octavia.amphorae.backends.agent.api_server import util
from octavia.amphorae.backends.health_daemon import health_sender
from octavia.amphorae.backends.utils import haproxy_query
from octavia.amphorae.backends.utils import keepalivedlvs_query
if six.PY2:
import Queue as queue # pylint: disable=wrong-import-order
@ -143,4 +145,28 @@ def build_stats_message():
pools = listener_dict['pools']
pools[pool_id] = {"status": pool['status'],
"members": pool['members']}
# UDP listener part
udp_listener_ids = util.get_udp_listeners()
if udp_listener_ids:
listeners_stats = keepalivedlvs_query.get_udp_listeners_stats()
if listeners_stats:
for listener_id, listener_stats in listeners_stats.items():
pool_status = keepalivedlvs_query.get_udp_listener_pool_status(
listener_id)
udp_listener_dict = dict()
udp_listener_dict['status'] = listener_stats['status']
udp_listener_dict['stats'] = {
'tx': listener_stats['stats']['bout'],
'rx': listener_stats['stats']['bin'],
'conns': listener_stats['stats']['scur'],
'totconns': listener_stats['stats']['stot'],
'ereq': listener_stats['stats']['ereq']
}
if pool_status:
udp_listener_dict['pools'] = {
pool_status['lvs']['uuid']: {
"status": pool_status['lvs']['status'],
"members": pool_status['lvs']['members']}}
msg['listeners'][listener_id] = udp_listener_dict
return msg

View File

@ -0,0 +1,421 @@
# 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 re
import subprocess
import netaddr
from oslo_log import log as logging
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants
LOG = logging.getLogger(__name__)
KERNEL_LVS_PATH = '/proc/net/ip_vs'
KERNEL_LVS_STATS_PATH = '/proc/net/ip_vs_stats'
LVS_KEY_REGEX = re.compile(r"RemoteAddress:Port\s+(.*$)")
V4_RS_VALUE_REGEX = re.compile(r"(\w{8}:\w{4})\s+(.*$)")
V4_HEX_IP_REGEX = re.compile(r"(\w{2})(\w{2})(\w{2})(\w{2})")
V6_RS_VALUE_REGEX = re.compile(r"(\[[[\w{4}:]+\b\]:\w{4})\s+(.*$)")
NS_REGEX = re.compile(r"net_namespace\s(\w+-\w+)")
V4_VS_REGEX = re.compile(r"virtual_server\s([\d+\.]+\b)\s(\d{1,5})")
V4_RS_REGEX = re.compile(r"real_server\s([\d+\.]+\b)\s(\d{1,5})")
V6_VS_REGEX = re.compile(r"virtual_server\s([\w*:]+\b)\s(\d{1,5})")
V6_RS_REGEX = re.compile(r"real_server\s([\w*:]+\b)\s(\d{1,5})")
CONFIG_COMMENT_REGEX = re.compile(
r"#\sConfiguration\sfor\s(\w+)\s(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})")
def read_kernel_file(ns_name, file_path):
cmd = ("ip netns exec {ns} cat {lvs_stat_path}".format(
ns=ns_name, lvs_stat_path=file_path))
try:
output = subprocess.check_output(cmd.split(),
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error("Failed to get kernel lvs status in ns %(ns_name)s "
"%(kernel_lvs_path)s: %(err)s %(out)s",
{'ns_name': ns_name, 'kernel_lvs_path': file_path,
'err': e, 'out': e.output})
raise e
# py3 treat the output as bytes type.
if isinstance(output, bytes):
output = output.decode('utf-8')
return output
def get_listener_realserver_mapping(ns_name, listener_ip_port):
# returned result:
# actual_member_result = {'rs_ip:listened_port': {
# 'status': 'UP',
# 'Forward': forward_type,
# 'Weight': 5,
# 'ActiveConn': 0,
# 'InActConn': 0
# }}
try:
listener_ip, listener_port = listener_ip_port.split(':')
except ValueError:
start = listener_ip_port.index('[') + 1
end = listener_ip_port.index(']')
listener_ip = listener_ip_port[start:end]
listener_port = listener_ip_port[end + 2:]
ip_obj = netaddr.IPAddress(listener_ip)
output = read_kernel_file(ns_name, KERNEL_LVS_PATH).split('\n')
ip_to_hex_format = ''
if ip_obj.version == 4:
for int_str in listener_ip.split('.'):
if int(int_str) <= 15:
str_piece = '0' + hex(int(int_str))[2:].upper()
else:
str_piece = hex(int(int_str))[2:].upper()
ip_to_hex_format += str_piece
elif ip_obj.version == 6:
piece_list = []
for word in ip_obj.words:
str_len = len(hex(word)[2:])
if str_len < 4:
str_piece = '0' * (4 - str_len) + hex(word)[2:].lower()
else:
str_piece = hex(word)[2:].lower()
piece_list.append(str_piece)
ip_to_hex_format = ":".join(piece_list)
ip_to_hex_format = '\[' + ip_to_hex_format + '\]'
port_hex_format = hex(int(listener_port))[2:].upper()
if len(port_hex_format) < 4:
port_hex_format = ('0' * (4 - len(port_hex_format)) +
port_hex_format)
idex = ip_to_hex_format + ':' + port_hex_format
def _hit_identify(line):
m = re.match(r'^UDP\s+%s\s+\w+' % idex, line)
if m:
return True
return False
actual_member_result = {}
find_target_block = False
result_keys = []
for line in output:
if 'RemoteAddress:Port' in line:
result_keys = re.split(r'\s+',
LVS_KEY_REGEX.findall(line)[0].strip())
elif line.startswith('UDP') and find_target_block:
break
elif line.startswith('UDP') and _hit_identify(line):
find_target_block = True
elif find_target_block and line:
rs_is_ipv4 = True
all_values = V4_RS_VALUE_REGEX.findall(line)
# If can not get all_values with ipv4 regex, then this line must be
# a ipv6 real server record.
if not all_values:
all_values = V6_RS_VALUE_REGEX.findall(line)
rs_is_ipv4 = False
all_values = all_values[0]
ip_port = all_values[0]
result_values = re.split(r"\s+", all_values[1].strip())
if rs_is_ipv4:
actual_member_ip_port = ip_port.split(':')
hex_ip_list = V4_HEX_IP_REGEX.findall(
actual_member_ip_port[0])[0]
ip_string = ''
for hex_ip in hex_ip_list:
ip_string = ip_string + str(int(hex_ip, 16)) + '.'
ip_string = ip_string[:-1]
port_string = str(int(actual_member_ip_port[1], 16))
member_ip_port_string = ip_string + ':' + port_string
else:
start = ip_port.index('[') + 1
end = ip_port.index(']')
ip_string = ip_port[start:end]
port_string = ip_port[end + 2:]
member_ip_port_string = '[' + str(
netaddr.IPAddress(ip_string)) + ']:' + str(
int(port_string, 16))
result_key_count = len(result_keys)
for index in range(result_key_count):
if member_ip_port_string not in actual_member_result:
actual_member_result[
member_ip_port_string] = {'status': constants.UP,
result_keys[index]:
result_values[index]}
else:
# The other values include the weight
actual_member_result[
member_ip_port_string][
result_keys[index]] = result_values[index]
continue
return find_target_block, actual_member_result
def get_udp_listener_resource_ipports_nsname(listener_id):
# resource_ipport_mapping = {'Listener': {'id': listener-id,
# 'ipport': ipport},
# 'Pool': {'id': pool-id},
# 'Members': [{'id': member-id-1,
# 'ipport': ipport},
# {'id': member-id-2,
# 'ipport': ipport}],
# 'HealthMonitor': {'id': healthmonitor-id}}
resource_ipport_mapping = {}
with open(util.keepalived_lvs_cfg_path(listener_id), 'r') as f:
cfg = f.read()
ns_name = NS_REGEX.findall(cfg)[0]
listener_ip_port = V4_VS_REGEX.findall(cfg)
if not listener_ip_port:
listener_ip_port = V6_VS_REGEX.findall(cfg)
listener_ip_port = listener_ip_port[0] if listener_ip_port else []
if not listener_ip_port:
# If not get listener_ip_port from the lvs config file,
# that means the udp listener's default pool have no enabled member
# yet. But at this moment, we can get listener_id and ns_name, so
# for this function, we will just return ns_name
return resource_ipport_mapping, ns_name
cfg_line = cfg.split('\n')
rs_ip_port_list = []
for line in cfg_line:
if 'real_server' in line:
res = V4_RS_REGEX.findall(line)
if not res:
res = V6_RS_REGEX.findall(line)
rs_ip_port_list.append(res[0])
resource_type_ids = CONFIG_COMMENT_REGEX.findall(cfg)
for resource_type, resource_id in resource_type_ids:
value = {'id': resource_id}
if resource_type == 'Member':
resource_type = '%ss' % resource_type
if resource_type not in resource_ipport_mapping:
value = [value]
if resource_type not in resource_ipport_mapping:
resource_ipport_mapping[resource_type] = value
elif resource_type == 'Members':
resource_ipport_mapping[resource_type].append(value)
if rs_ip_port_list:
rs_ip_port_count = len(rs_ip_port_list)
for index in range(rs_ip_port_count):
if netaddr.IPAddress(
rs_ip_port_list[index][0]).version == 6:
rs_ip_port_list[index] = (
'[' + rs_ip_port_list[index][0] + ']',
rs_ip_port_list[index][1])
resource_ipport_mapping['Members'][index]['ipport'] = (
rs_ip_port_list[index][0] + ':' +
rs_ip_port_list[index][1])
if netaddr.IPAddress(listener_ip_port[0]).version == 6:
listener_ip_port = (
'[' + listener_ip_port[0] + ']', listener_ip_port[1])
resource_ipport_mapping['Listener']['ipport'] = (
listener_ip_port[0] + ':' + listener_ip_port[1])
return resource_ipport_mapping, ns_name
def get_udp_listener_pool_status(listener_id):
(resource_ipport_mapping,
ns_name) = get_udp_listener_resource_ipports_nsname(listener_id)
if 'Pool' not in resource_ipport_mapping:
return {}
elif 'Members' not in resource_ipport_mapping:
return {'lvs': {
'uuid': resource_ipport_mapping['Pool']['id'],
'status': constants.DOWN,
'members': {}
}}
_, realserver_result = get_listener_realserver_mapping(
ns_name, resource_ipport_mapping['Listener']['ipport'])
pool_status = constants.UP
member_results = {}
if realserver_result:
member_ip_port_list = [
member['ipport'] for member in resource_ipport_mapping['Members']]
down_member_ip_port_set = set(
member_ip_port_list) - set(list(realserver_result.keys()))
for member_ip_port in member_ip_port_list:
member_id = None
for member in resource_ipport_mapping['Members']:
if member['ipport'] == member_ip_port:
member_id = member['id']
if member_ip_port in down_member_ip_port_set:
status = constants.DOWN
elif int(realserver_result[member_ip_port]['Weight']) == 0:
status = constants.DRAIN
else:
status = realserver_result[member_ip_port]['status']
if member_id:
member_results[member_id] = status
else:
pool_status = constants.DOWN
for member in resource_ipport_mapping['Members']:
member_results[member['id']] = constants.DOWN
return {
'lvs':
{
'uuid': resource_ipport_mapping['Pool']['id'],
'status': pool_status,
'members': member_results
}
}
def get_ipvsadm_info(ns_name, is_stats_cmd=False):
cmd_list = ['ip', 'netns', 'exec', ns_name, 'ipvsadm', '-Ln']
if is_stats_cmd:
cmd_list.append('--stats')
output = subprocess.check_output(cmd_list, stderr=subprocess.STDOUT)
if isinstance(output, bytes):
output = output.decode('utf-8')
output = output.split('\n')
fields = []
# mapping = {'listeneripport': {'Linstener': vs_values,
# 'members': [rs_values1, rs_values2]}}
last_key = None
value_mapping = dict()
output_line_num = len(output)
def split_line(line):
return re.sub(r'\s+', ' ', line.strip()).split(' ')
for line_num in range(output_line_num):
# ipvsadm -Ln
if 'Flags' in output[line_num]:
fields = split_line(output[line_num])
elif fields and 'Flags' in fields and fields.index('Flags') == len(
fields) - 1:
fields.extend(split_line(output[line_num]))
# ipvsadm -Ln --stats
elif 'Prot' in output[line_num]:
fields = split_line(output[line_num])
elif 'RemoteAddress' in output[line_num]:
start = fields.index('LocalAddress:Port') + 1
temp_fields = fields[start:]
fields.extend(split_line(output[line_num]))
fields.extend(temp_fields)
# here we get the all fields
elif constants.PROTOCOL_UDP in output[line_num]:
# if UDP/TCP in this line, we can know this line is
# VS configuration.
vs_values = split_line(output[line_num])
for value in vs_values:
if ':' in value:
value_mapping[value] = {'Listener': vs_values,
'Members': []}
last_key = value
break
# here the line must be a RS which belongs to a VS
elif '->' in output[line_num] and last_key:
rs_values = split_line(output[line_num])
rs_values.remove('->')
value_mapping[last_key]['Members'].append(rs_values)
index = fields.index('->')
vs_fields = fields[:index]
if 'Flags' in vs_fields:
vs_fields.remove('Flags')
rs_fields = fields[index + 1:]
for key in list(value_mapping.keys()):
value_mapping[key]['Listener'] = [
i for i in zip(vs_fields, value_mapping[key]['Listener'])]
member_res = []
for member_value in value_mapping[key]['Members']:
member_res.append([i for i in zip(rs_fields, member_value)])
value_mapping[key]['Members'] = member_res
return value_mapping
def get_udp_listeners_stats():
udp_listener_ids = util.get_udp_listeners()
need_check_listener_ids = [
listener_id for listener_id in udp_listener_ids
if util.is_udp_listener_running(listener_id)]
ipport_mapping = dict()
for check_listener_id in need_check_listener_ids:
# resource_ipport_mapping = {'Listener': {'id': listener-id,
# 'ipport': ipport},
# 'Pool': {'id': pool-id},
# 'Members': [{'id': member-id-1,
# 'ipport': ipport},
# {'id': member-id-2,
# 'ipport': ipport}],
# 'HealthMonitor': {'id': healthmonitor-id}}
(resource_ipport_mapping,
ns_name) = get_udp_listener_resource_ipports_nsname(check_listener_id)
# If we can not read the lvs configuration from file, that means
# the pool of this listener may own zero enabled member, but the
# keepalived process is running. So we need to skip it.
if not resource_ipport_mapping:
continue
ipport_mapping.update({check_listener_id: resource_ipport_mapping})
# So here, if we can not get any ipport_mapping,
# we do nothing, just return
if not ipport_mapping:
return None
# contains bout, bin, scur, stot, ereq, status
# bout(OutBytes), bin(InBytes), stot(Conns) from cmd ipvsadm -Ln --stats
# scur(ActiveConn) from cmd ipvsadm -Ln
# status, can see configuration in any cmd, treat it as OPEN
# ereq is still 0, as UDP case does not support it.
scur_res = get_ipvsadm_info(constants.AMPHORA_NAMESPACE)
stats_res = get_ipvsadm_info(constants.AMPHORA_NAMESPACE,
is_stats_cmd=True)
listener_stats_res = dict()
for listener_id, ipport in ipport_mapping.items():
listener_ipport = ipport['Listener']['ipport']
# This would be in Error, wait for the next loop to sync for the
# listener at this moment. Also this is for skip the case no enabled
# member in UDP listener, so we don't check it for failover.
if listener_ipport not in scur_res or listener_ipport not in stats_res:
continue
scur, bout, bin, stot, ereq = 0, 0, 0, 0, 0
# As all results contain this listener, so its status should be OPEN
status = constants.OPEN
# Get scur
for m in scur_res[listener_ipport]['Members']:
for item in m:
if item[0] == 'ActiveConn':
scur += int(item[1])
# Get bout, bin, stot
for item in stats_res[listener_ipport]['Listener']:
if item[0] == 'Conns':
stot = int(item[1])
elif item[0] == 'OutBytes':
bout = int(item[1])
elif item[0] == 'InBytes':
bin = int(item[1])
listener_stats_res.update({
listener_id: {
'stats': {
'bout': bout,
'bin': bin,
'scur': scur,
'stot': stot,
'ereq': ereq},
'status': status}})
return listener_stats_res

View File

@ -30,6 +30,7 @@ from octavia.amphorae.drivers.keepalived import vrrp_rest_driver
from octavia.common.config import cfg
from octavia.common import constants as consts
from octavia.common.jinja.haproxy import jinja_cfg
from octavia.common.jinja.lvs import jinja_cfg as jinja_udp_cfg
from octavia.common.tls_utils import cert_parser
from octavia.common import utils
@ -59,6 +60,7 @@ class HaproxyAmphoraLoadBalancerDriver(
base_crt_dir=CONF.haproxy_amphora.base_cert_dir,
haproxy_template=CONF.haproxy_amphora.haproxy_template,
connection_logging=CONF.haproxy_amphora.connection_logging)
self.udp_jinja = jinja_udp_cfg.LvsJinjaTemplater()
def update_amphora_listeners(self, listeners, amphora_index,
amphorae, timeout_dict=None):
@ -87,36 +89,64 @@ class HaproxyAmphoraLoadBalancerDriver(
for listener in listeners:
LOG.debug("%s updating listener %s on amphora %s",
self.__class__.__name__, listener.id, amp.id)
certs = self._process_tls_certificates(listener)
# Generate HaProxy configuration from listener object
config = self.jinja.build_config(
host_amphora=amp,
listener=listener,
tls_cert=certs['tls_cert'],
user_group=CONF.haproxy_amphora.user_group)
self.client.upload_config(amp, listener.id, config,
timeout_dict=timeout_dict)
self.client.reload_listener(amp, listener.id,
timeout_dict=timeout_dict)
def update(self, listener, vip):
LOG.debug("Amphora %s haproxy, updating listener %s, vip %s",
self.__class__.__name__, listener.protocol_port,
vip.ip_address)
# Process listener certificate info
certs = self._process_tls_certificates(listener)
for amp in listener.load_balancer.amphorae:
if amp.status != consts.DELETED:
if listener.protocol == 'UDP':
# Generate Keepalived LVS configuration from listener object
config = self.udp_jinja.build_config(listener=listener)
self.client.upload_udp_config(amp, listener.id, config,
timeout_dict=timeout_dict)
self.client.reload_listener(amp, listener.id,
listener.protocol,
timeout_dict=timeout_dict)
else:
certs = self._process_tls_certificates(listener)
# Generate HaProxy configuration from listener object
config = self.jinja.build_config(
host_amphora=amp,
listener=listener,
tls_cert=certs['tls_cert'],
user_group=CONF.haproxy_amphora.user_group)
self.client.upload_config(amp, listener.id, config)
self.client.reload_listener(amp, listener.id)
self.client.upload_config(amp, listener.id, config,
timeout_dict=timeout_dict)
self.client.reload_listener(amp, listener.id,
timeout_dict=timeout_dict)
def _udp_update(self, listener, vip):
LOG.debug("Amphora %s keepalivedlvs, updating "
"listener %s, vip %s",
self.__class__.__name__, listener.protocol_port,
vip.ip_address)
for amp in listener.load_balancer.amphorae:
if amp.status != consts.DELETED:
# Generate Keepalived LVS configuration from listener object
config = self.udp_jinja.build_config(listener=listener)
self.client.upload_udp_config(amp, listener.id, config)
self.client.reload_listener(amp, listener.id,
listener.protocol)
def update(self, listener, vip):
if listener.protocol == 'UDP':
self._udp_update(listener, vip)
else:
LOG.debug("Amphora %s haproxy, updating listener %s, "
"vip %s", self.__class__.__name__,
listener.protocol_port,
vip.ip_address)
# Process listener certificate info
certs = self._process_tls_certificates(listener)
for amp in listener.load_balancer.amphorae:
if amp.status != consts.DELETED:
# Generate HaProxy configuration from listener object
config = self.jinja.build_config(
host_amphora=amp,
listener=listener,
tls_cert=certs['tls_cert'],
user_group=CONF.haproxy_amphora.user_group)
self.client.upload_config(amp, listener.id, config)
self.client.reload_listener(amp, listener.id,
listener.protocol)
def upload_cert_amp(self, amp, pem):
LOG.debug("Amphora %s updating cert in REST driver "
@ -124,13 +154,37 @@ class HaproxyAmphoraLoadBalancerDriver(
self.__class__.__name__, amp.id)
self.client.update_cert_for_rotation(amp, pem)
def _check_if_need_add_listener_protocol(self, func):
# as _apply func will be called by create/update/delete and some cert
# related function. But there is only a port of them can accept
# listener protocol parameter, including:
# start/stop functions call _apply by functools.
# delete function call _apply directly.
# escape cert operation based on function name.
# So add this check for verify if the target function need a protocol
# parameter.
called_by_functools = (not hasattr(func, '__name__') and
hasattr(func, 'func') and
func.func.__name__.find('cert') < 0)
called_directly = (hasattr(func, '__name__') and
func.__name__.find('cert') < 0)
return called_directly or called_by_functools
def _apply(self, func, listener=None, amphora=None, *args):
if amphora is None:
for amp in listener.load_balancer.amphorae:
if amp.status != consts.DELETED:
if self._check_if_need_add_listener_protocol(func):
_list = list(args)
_list.append(listener.protocol)
args = _list
func(amp, listener.id, *args)
else:
if amphora.status != consts.DELETED:
if self._check_if_need_add_listener_protocol(func):
_list = list(args)
_list.append(listener.protocol)
args = _list
func(amphora, listener.id, *args)
def stop(self, listener, vip):
@ -374,17 +428,21 @@ class AmphoraAPIClient(object):
data=config)
return exc.check_exception(r)
def get_listener_status(self, amp, listener_id):
def get_listener_status(self, amp, listener_id, protocol=None):
protocol_dict = {'protocol': protocol}
r = self.get(
amp,
'listeners/{listener_id}'.format(listener_id=listener_id))
'listeners/{listener_id}'.format(listener_id=listener_id),
json=protocol_dict)
if exc.check_exception(r):
return r.json()
return None
def _action(self, action, amp, listener_id, timeout_dict=None):
def _action(self, action, amp, listener_id, protocol, timeout_dict=None):
protocol_dict = {'protocol': protocol}
r = self.put(amp, 'listeners/{listener_id}/{action}'.format(
listener_id=listener_id, action=action), timeout_dict=timeout_dict)
listener_id=listener_id, action=action), timeout_dict=timeout_dict,
json=protocol_dict)
return exc.check_exception(r)
def upload_cert_pem(self, amp, listener_id, pem_filename, pem_file):
@ -407,9 +465,11 @@ class AmphoraAPIClient(object):
return r.json().get("md5sum")
return None
def delete_listener(self, amp, listener_id):
def delete_listener(self, amp, listener_id, protocol):
protocol_dict = {'protocol': protocol}
r = self.delete(
amp, 'listeners/{listener_id}'.format(listener_id=listener_id))
amp, 'listeners/{listener_id}'.format(listener_id=listener_id),
json=protocol_dict)
return exc.check_exception(r, (404,))
@ -463,3 +523,11 @@ class AmphoraAPIClient(object):
if exc.check_exception(r):
return r.json()
return None
def upload_udp_config(self, amp, listener_id, config, timeout_dict=None):
r = self.put(
amp,
'listeners/{amphora_id}/{listener_id}/udp_listener'.format(
amphora_id=amp.id, listener_id=listener_id), timeout_dict,
data=config)
return exc.check_exception(r)

View File

@ -135,6 +135,9 @@ amphora_agent_opts = [
"controller to run before terminating the socket.")),
# Do not specify in octavia.conf, loaded at runtime
cfg.StrOpt('amphora_id', help=_("The amphora ID.")),
cfg.StrOpt('amphora_udp_driver',
default='keepalived_lvs',
help='The UDP API backend for amphora agent.'),
]
networking_opts = [

View File

@ -438,6 +438,7 @@ KEEPALIVED_JINJA2_UPSTART = 'keepalived.upstart.j2'
KEEPALIVED_JINJA2_SYSTEMD = 'keepalived.systemd.j2'
KEEPALIVED_JINJA2_SYSVINIT = 'keepalived.sysvinit.j2'
CHECK_SCRIPT_CONF = 'keepalived_check_script.conf.j2'
KEEPALIVED_CHECK_SCRIPT = 'keepalived_lvs_check_script.sh.j2'
PLUGGED_INTERFACES = '/var/lib/octavia/plugged_interfaces'
HAPROXY_USER_GROUP_CFG = '/var/lib/octavia/haproxy-default-user-group.conf'
@ -482,6 +483,10 @@ KEEPALIVED_SYSTEMD = 'octavia-keepalived.service'
KEEPALIVED_SYSVINIT = 'octavia-keepalived'
KEEPALIVED_UPSTART = 'octavia-keepalived.conf'
KEEPALIVED_SYSTEMD_PREFIX = 'octavia-keepalivedlvs-%s.service'
KEEPALIVED_SYSVINIT_PREFIX = 'octavia-keepalivedlvs-%s'
KEEPALIVED_UPSTART_PREFIX = 'octavia-keepalivedlvs-%s.conf'
# Authentication
KEYSTONE = 'keystone'
NOAUTH = 'noauth'

View File

@ -118,6 +118,31 @@ class UpdateHealthDb(update_base.HealthUpdateBase):
if db_lb:
expected_listener_count = len(db_lb.listeners)
# For udp listener, the udp health won't send out by amp agent.
# Once the default_pool of udp listener have the first enabled
# member, then the health will be sent out. So during this period,
# need to figure out the udp listener and ignore them by changing
# expected_listener_count.
for listener in db_lb.listeners:
need_remove = False
if listener.protocol == constants.PROTOCOL_UDP:
enabled_members = ([member
for member in
listener.default_pool.members
if member.enabled]
if listener.default_pool else [])
if listener.default_pool:
if not listener.default_pool.members:
need_remove = True
elif not enabled_members:
need_remove = True
else:
need_remove = True
if need_remove:
expected_listener_count = expected_listener_count - 1
if 'PENDING' in db_lb.provisioning_status:
ignore_listener_count = True
else:

View File

@ -384,6 +384,16 @@ class AmphoraFlows(object):
failover_amphora_flow.add(database_tasks.GetAmphoraeFromLoadbalancer(
requires=constants.LOADBALANCER, provides=constants.AMPHORAE))
# Plug the VIP ports into the new amphora
# The reason for moving these steps here is the udp listeners want to
# do some kernel configuration before Listener update for forbidding
# failure during rebuild amphora.
failover_amphora_flow.add(network_tasks.PlugVIPPort(
requires=(constants.AMPHORA, constants.AMPHORAE_NETWORK_CONFIG)))
failover_amphora_flow.add(amphora_driver_tasks.AmphoraPostVIPPlug(
requires=(constants.AMPHORA, constants.LOADBALANCER,
constants.AMPHORAE_NETWORK_CONFIG)))
# Listeners update needs to be run on all amphora to update
# their peer configurations. So parallelize this with an
# unordered subflow.
@ -414,13 +424,6 @@ class AmphoraFlows(object):
failover_amphora_flow.add(update_amps_subflow)
# Plug the VIP ports into the new amphora
failover_amphora_flow.add(network_tasks.PlugVIPPort(
requires=(constants.AMPHORA, constants.AMPHORAE_NETWORK_CONFIG)))
failover_amphora_flow.add(amphora_driver_tasks.AmphoraPostVIPPlug(
requires=(constants.AMPHORA, constants.LOADBALANCER,
constants.AMPHORAE_NETWORK_CONFIG)))
# Plug the member networks into the new amphora
failover_amphora_flow.add(network_tasks.CalculateAmphoraDelta(
requires=(constants.LOADBALANCER, constants.AMPHORA),

View File

@ -136,29 +136,41 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
rules = self.neutron_client.list_security_group_rules(
security_group_id=sec_grp_id)
updated_ports = [
listener.protocol_port for listener in load_balancer.listeners
(listener.protocol_port,
constants.PROTOCOL_TCP.lower()
if listener.protocol != constants.PROTOCOL_UDP else
constants.PROTOCOL_UDP.lower())
for listener in load_balancer.listeners
if listener.provisioning_status != constants.PENDING_DELETE and
listener.provisioning_status != constants.DELETED]
# As the peer port will hold the tcp connection for keepalived and
# haproxy session synchronization, so here the security group rule
# should be just related with tcp protocol only.
peer_ports = [
listener.peer_port for listener in load_balancer.listeners
(listener.peer_port,
constants.PROTOCOL_TCP.lower())
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
old_ports = [rule.get('port_range_max')
old_ports = [(rule.get('port_range_max'), rule.get('protocol'))
for rule in rules.get('security_group_rules', [])
# Don't remove egress rules and don't
# confuse other protocols with None ports
# with the egress rules. VRRP uses protocol
# 51 and 112
if rule.get('direction') != 'egress' and
rule.get('protocol', '').lower() == 'tcp']
rule.get('protocol', '').lower() in ['tcp', 'udp']]
add_ports = set(updated_ports) - set(old_ports)
del_ports = set(old_ports) - set(updated_ports)
for rule in rules.get('security_group_rules', []):
if rule.get('port_range_max') in del_ports:
if (rule.get('protocol', '') and
rule.get('protocol', '').lower() in ['tcp', 'udp'] and
(rule.get('port_range_max'),
rule.get('protocol')) in del_ports):
rule_id = rule.get('id')
try:
self.neutron_client.delete_security_group_rule(rule_id)
@ -167,9 +179,10 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
"it is already deleted.", rule_id)
ethertype = self._get_ethertype_for_ip(load_balancer.vip.ip_address)
for port in add_ports:
self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port,
port_max=port,
for port_protocol in add_ports:
self._create_security_group_rule(sec_grp_id, port_protocol[1],
port_min=port_protocol[0],
port_max=port_protocol[0],
ethertype=ethertype)
# Currently we are using the VIP network for VRRP

View File

@ -57,6 +57,9 @@ class TestServerTestCase(base.TestCase):
self.conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF))
self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia')
mock.patch('octavia.amphorae.backends.agent.api_server.server.'
'check_and_return_request_listener_protocol',
return_value='TCP').start()
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSTEMD)
@ -374,9 +377,13 @@ class TestServerTestCase(base.TestCase):
def test_centos_info(self):
self._test_info(consts.CENTOS)
@mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.'
'AmphoraInfo._get_extend_body_from_udp_driver',
return_value={})
@mock.patch('socket.gethostname')
@mock.patch('subprocess.check_output')
def _test_info(self, distro, mock_subbprocess, mock_hostname):
def _test_info(self, distro, mock_subbprocess, mock_hostname,
mock_get_extend_body):
self.assertIn(distro, [consts.UBUNTU, consts.CENTOS])
mock_hostname.side_effect = ['test-host']
mock_subbprocess.side_effect = ['9.9.99-9']
@ -990,6 +997,7 @@ class TestServerTestCase(base.TestCase):
agent_server_network_file="/path/to/interfaces_file")
self._test_plug_network(consts.CENTOS)
@mock.patch('os.chmod')
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@ -1000,7 +1008,7 @@ class TestServerTestCase(base.TestCase):
@mock.patch('os.path.isfile')
def _test_plug_network(self, distro, mock_isfile, mock_int_exists,
mock_check_output, mock_netns, mock_pyroute2,
mock_ifaddress, mock_interfaces):
mock_ifaddress, mock_interfaces, mock_os_chmod):
self.assertIn(distro, [consts.UBUNTU, consts.CENTOS])
port_info = {'mac_address': '123'}
test_int_num = random.randint(0, 9999)
@ -1205,7 +1213,11 @@ class TestServerTestCase(base.TestCase):
'iface eth{int} inet static\n'
'address 10.0.0.5\nbroadcast 10.0.0.255\n'
'netmask 255.255.255.0\n'
'mtu 1450\n'.format(int=test_int_num))
'mtu 1450\n'
'post-up /sbin/iptables -t nat -A POSTROUTING -p udp '
'-o eth{int} -j MASQUERADE\n'
'post-down /sbin/iptables -t nat -D POSTROUTING -p udp '
'-o eth{int} -j MASQUERADE\n'.format(int=test_int_num))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
'\n\n# Generated by Octavia agent\n'
@ -1279,7 +1291,11 @@ class TestServerTestCase(base.TestCase):
'iface eth{int} inet6 static\n'
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32\nmtu 1450\n'.format(int=test_int_num))
'netmask 32\nmtu 1450\n'
'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp '
'-o eth{int} -j MASQUERADE\n'
'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp '
'-o eth{int} -j MASQUERADE\n'.format(int=test_int_num))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
'\n\n# Generated by Octavia agent\n'
@ -1384,6 +1400,7 @@ class TestServerTestCase(base.TestCase):
def test_centos_plug_network_host_routes(self):
self._test_plug_network_host_routes(consts.CENTOS)
@mock.patch('os.chmod')
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@ -1391,7 +1408,8 @@ class TestServerTestCase(base.TestCase):
@mock.patch('subprocess.check_output')
def _test_plug_network_host_routes(self, distro, mock_check_output,
mock_netns, mock_pyroute2,
mock_ifaddress, mock_interfaces):
mock_ifaddress, mock_interfaces,
mock_os_chmod):
self.assertIn(distro, [consts.UBUNTU, consts.CENTOS])
@ -1466,8 +1484,12 @@ class TestServerTestCase(base.TestCase):
'up route add -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'down route del -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
)
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' +
'post-up /sbin/iptables -t nat -A POSTROUTING -p udp -o ' +
consts.NETNS_PRIMARY_INTERFACE + ' -j MASQUERADE' + '\n' +
'post-down /sbin/iptables -t nat -D POSTROUTING -p udp '
'-o ' + consts.NETNS_PRIMARY_INTERFACE +
' -j MASQUERADE' + '\n')
elif distro == consts.CENTOS:
handle.write.assert_any_call(
'\n\n# Generated by Octavia agent\n'
@ -1479,6 +1501,12 @@ class TestServerTestCase(base.TestCase):
int=consts.NETNS_PRIMARY_INTERFACE,
ip=IP,
mask=NETMASK))
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mock_open.assert_any_call('/sbin/ifup-local', flags, mode)
mock_open.assert_any_call('/sbin/ifdown-local', flags, mode)
calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC),
mock.call('/sbin/ifdown-local', stat.S_IEXEC)]
mock_os_chmod.assert_has_calls(calls)
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2)
@ -1494,6 +1522,7 @@ class TestServerTestCase(base.TestCase):
agent_server_network_file="/path/to/interfaces_file")
self._test_plug_VIP4(consts.CENTOS)
@mock.patch('os.chmod')
@mock.patch('shutil.copy2')
@mock.patch('pyroute2.NSPopen')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
@ -1511,7 +1540,7 @@ class TestServerTestCase(base.TestCase):
mock_copytree, mock_check_output, mock_netns,
mock_netns_create, mock_pyroute2, mock_ifaddress,
mock_interfaces, mock_int_exists, mock_nspopen,
mock_copy2):
mock_copy2, mock_os_chmod):
mock_isfile.return_value = True
@ -1717,7 +1746,11 @@ class TestServerTestCase(base.TestCase):
'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 '
'priority 100\n'
'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 '
'priority 100'.format(
'priority 100\n'
'post-up /sbin/iptables -t nat -A POSTROUTING -p udp '
'-o eth1 -j MASQUERADE\n'
'post-down /sbin/iptables -t nat -D POSTROUTING -p udp '
'-o eth1 -j MASQUERADE'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
@ -1728,15 +1761,29 @@ class TestServerTestCase(base.TestCase):
'NETMASK="255.255.255.0"\nGATEWAY="203.0.113.1"\n'
'MTU="1450" \n'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mock_open.assert_any_call('/sbin/ifup-local', flags, mode)
mock_open.assert_any_call('/sbin/ifdown-local', flags, mode)
calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC),
mock.call('/sbin/ifdown-local', stat.S_IEXEC)]
mock_os_chmod.assert_has_calls(calls)
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', '{netns_int}:0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# Verify sysctl was loaded
mock_nspopen.assert_called_once_with(
'amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE)
calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy', ['modprobe', 'ip_vs'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.ip_forward=1'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'],
stdout=subprocess.PIPE)]
mock_nspopen.assert_has_calls(calls, any_order=True)
# One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']]
@ -1810,7 +1857,11 @@ class TestServerTestCase(base.TestCase):
'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 '
'priority 100\n'
'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 '
'priority 100'.format(
'priority 100\n'
'post-up /sbin/iptables -t nat -A POSTROUTING -p udp '
'-o eth1 -j MASQUERADE\n'
'post-down /sbin/iptables -t nat -D POSTROUTING -p udp '
'-o eth1 -j MASQUERADE'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
@ -1857,6 +1908,7 @@ class TestServerTestCase(base.TestCase):
def test_centos_plug_VIP6(self):
self._test_plug_vip6(consts.CENTOS)
@mock.patch('os.chmod')
@mock.patch('shutil.copy2')
@mock.patch('pyroute2.NSPopen')
@mock.patch('netifaces.interfaces')
@ -1871,7 +1923,8 @@ class TestServerTestCase(base.TestCase):
def _test_plug_vip6(self, distro, mock_isfile, mock_makedirs,
mock_copytree, mock_check_output, mock_netns,
mock_netns_create, mock_pyroute2, mock_ifaddress,
mock_interfaces, mock_nspopen, mock_copy2):
mock_interfaces, mock_nspopen, mock_copy2,
mock_os_chmod):
mock_isfile.return_value = True
@ -2059,7 +2112,11 @@ class TestServerTestCase(base.TestCase):
'priority 100\n'
'post-down /sbin/ip -6 rule del from '
'2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 '
'priority 100'.format(
'priority 100\n'
'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp '
'-o eth1 -j MASQUERADE\n'
'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp '
'-o eth1 -j MASQUERADE'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
@ -2085,9 +2142,18 @@ class TestServerTestCase(base.TestCase):
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# Verify sysctl was loaded
mock_nspopen.assert_called_once_with(
'amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE)
calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy', ['modprobe', 'ip_vs'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w',
'net.ipv6.conf.all.forwarding=1'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'],
stdout=subprocess.PIPE)]
mock_nspopen.assert_has_calls(calls, any_order=True)
# One Interface down, Happy Path IPv6
mock_interfaces.side_effect = [['blah']]
@ -2157,7 +2223,11 @@ class TestServerTestCase(base.TestCase):
'priority 100\n'
'post-down /sbin/ip -6 rule del from '
'2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 '
'priority 100'.format(
'priority 100\n'
'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp '
'-o eth1 -j MASQUERADE\n'
'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp '
'-o eth1 -j MASQUERADE'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
elif distro == consts.CENTOS:
handle.write.assert_any_call(
@ -2401,6 +2471,19 @@ class TestServerTestCase(base.TestCase):
def test_centos_details(self):
self._test_details(consts.CENTOS)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_udp_listeners',
return_value=[])
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo.'
'_get_extend_body_from_udp_driver',
return_value={
"keepalived_version": '1.1.11-1',
"ipvsadm_version": '2.2.22-2'
})
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo.'
'_count_udp_listener_processes', return_value=0)
@mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.'
'AmphoraInfo._count_haproxy_processes')
@mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.'
@ -2419,7 +2502,8 @@ class TestServerTestCase(base.TestCase):
def _test_details(self, distro, mock_subbprocess, mock_hostname,
mock_get_listeners, mock_get_mem, mock_cpu,
mock_statvfs, mock_load, mock_get_nets,
mock_count_haproxy):
mock_count_haproxy, mock_count_udp_listeners,
mock_get_ext_from_udp_driver, mock_get_udp_listeners):
self.assertIn(distro, [consts.UBUNTU, consts.CENTOS])
@ -2514,6 +2598,8 @@ class TestServerTestCase(base.TestCase):
'haproxy_count': haproxy_count,
'haproxy_version': '9.9.99-9',
'hostname': 'test-host',
'ipvsadm_version': u'2.2.22-2',
'keepalived_version': u'1.1.11-1',
'listeners': [listener_id],
'load': [load_1min, load_5min, load_15min],
'memory': {'buffers': Buffers,
@ -2531,7 +2617,8 @@ class TestServerTestCase(base.TestCase):
'network_tx': eth3_tx}},
'packages': {},
'topology': consts.TOPOLOGY_SINGLE,
'topology_status': consts.TOPOLOGY_STATUS_OK}
'topology_status': consts.TOPOLOGY_STATUS_OK,
'udp_listener_process_count': 0}
if distro == consts.UBUNTU:
rv = self.ubuntu_app.get('/' + api_server.VERSION + '/details')

View File

@ -27,11 +27,26 @@ class TestAmphoraInfo(base.TestCase):
API_VERSION = random.randrange(0, 10000)
HAPROXY_VERSION = random.randrange(0, 10000)
KEEPALIVED_VERSION = random.randrange(0, 10000)
IPVSADM_VERSION = random.randrange(0, 10000)
FAKE_LISTENER_ID_1 = uuidutils.generate_uuid()
FAKE_LISTENER_ID_2 = uuidutils.generate_uuid()
FAKE_LISTENER_ID_3 = uuidutils.generate_uuid()
FAKE_LISTENER_ID_4 = uuidutils.generate_uuid()
def setUp(self):
super(TestAmphoraInfo, self).setUp()
self.osutils_mock = mock.MagicMock()
self.amp_info = amphora_info.AmphoraInfo(self.osutils_mock)
self.udp_driver = mock.MagicMock()
def _return_version(self, package_name):
if package_name == 'ipvsadm':
return self.IPVSADM_VERSION
elif package_name == 'keepalived':
return self.KEEPALIVED_VERSION
else:
return self.HAPROXY_VERSION
@mock.patch.object(amphora_info, "webob")
@mock.patch('octavia.amphorae.backends.agent.api_server.'
@ -49,6 +64,186 @@ class TestAmphoraInfo(base.TestCase):
mock_webob.Response.assert_called_once_with(json=expected_dict)
api_server.VERSION = original_version
@mock.patch.object(amphora_info, "webob")
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_version_of_installed_package')
@mock.patch('socket.gethostname', return_value='FAKE_HOST')
def test_compile_amphora_info_for_udp(self, mock_gethostname,
mock_pkg_version, mock_webob):
mock_pkg_version.side_effect = self._return_version
self.udp_driver.get_subscribed_amp_compile_info.side_effect = [
['keepalived', 'ipvsadm']]
original_version = api_server.VERSION
api_server.VERSION = self.API_VERSION
expected_dict = {'api_version': self.API_VERSION,
'hostname': 'FAKE_HOST',
'haproxy_version': self.HAPROXY_VERSION,
'keepalived_version': self.KEEPALIVED_VERSION,
'ipvsadm_version': self.IPVSADM_VERSION
}
self.amp_info.compile_amphora_info(extend_udp_driver=self.udp_driver)
mock_webob.Response.assert_called_once_with(json=expected_dict)
api_server.VERSION = original_version
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_listeners', return_value=[FAKE_LISTENER_ID_1,
FAKE_LISTENER_ID_2])
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_meminfo')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._cpu')
@mock.patch('os.statvfs')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_networks')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._load')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_version_of_installed_package')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._count_haproxy_processes')
@mock.patch('socket.gethostname', return_value='FAKE_HOST')
def test_compile_amphora_details(self, mhostname, m_count, m_pkg_version,
m_load, m_get_nets, m_os, m_cpu,
mget_mem, mget_listener):
mget_mem.return_value = {'SwapCached': 0, 'Buffers': 344792,
'MemTotal': 21692784, 'Cached': 4271856,
'Slab': 534384, 'MemFree': 12685624,
'Shmem': 9520}
m_cpu.return_value = {'user': '252551', 'softirq': '8336',
'system': '52554', 'total': 7503411}
m_pkg_version.side_effect = self._return_version
mdisk_info = mock.MagicMock()
m_os.return_value = mdisk_info
mdisk_info.f_blocks = 34676992
mdisk_info.f_bfree = 28398016
mdisk_info.f_frsize = 4096
mdisk_info.f_bavail = 26630646
m_get_nets.return_value = {'eth1': {'network_rx': 996,
'network_tx': 418},
'eth2': {'network_rx': 848,
'network_tx': 578}}
m_load.return_value = ['0.09', '0.11', '0.10']
m_count.return_value = 5
original_version = api_server.VERSION
api_server.VERSION = self.API_VERSION
expected_dict = {u'active': True,
u'api_version': self.API_VERSION,
u'cpu': {u'soft_irq': u'8336',
u'system': u'52554',
u'total': 7503411,
u'user': u'252551'},
u'disk': {u'available': 109079126016,
u'used': 25718685696},
u'haproxy_count': 5,
u'haproxy_version': self.HAPROXY_VERSION,
u'hostname': u'FAKE_HOST',
u'listeners': [self.FAKE_LISTENER_ID_1,
self.FAKE_LISTENER_ID_2],
u'load': [u'0.09', u'0.11', u'0.10'],
u'memory': {u'buffers': 344792,
u'cached': 4271856,
u'free': 12685624,
u'shared': 9520,
u'slab': 534384,
u'swap_used': 0,
u'total': 21692784},
u'networks': {u'eth1': {u'network_rx': 996,
u'network_tx': 418},
u'eth2': {u'network_rx': 848,
u'network_tx': 578}},
u'packages': {},
u'topology': u'SINGLE',
u'topology_status': u'OK'}
actual = self.amp_info.compile_amphora_details()
self.assertEqual(expected_dict, actual.json)
api_server.VERSION = original_version
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_udp_listeners',
return_value=[FAKE_LISTENER_ID_3, FAKE_LISTENER_ID_4])
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_listeners', return_value=[FAKE_LISTENER_ID_1,
FAKE_LISTENER_ID_2])
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_meminfo')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._cpu')
@mock.patch('os.statvfs')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_networks')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._load')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_version_of_installed_package')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._count_haproxy_processes')
@mock.patch('socket.gethostname', return_value='FAKE_HOST')
def test_compile_amphora_details_for_udp(self, mhostname, m_count,
m_pkg_version, m_load, m_get_nets,
m_os, m_cpu, mget_mem,
mget_listener, mget_udp_listener):
mget_mem.return_value = {'SwapCached': 0, 'Buffers': 344792,
'MemTotal': 21692784, 'Cached': 4271856,
'Slab': 534384, 'MemFree': 12685624,
'Shmem': 9520}
m_cpu.return_value = {'user': '252551', 'softirq': '8336',
'system': '52554', 'total': 7503411}
m_pkg_version.side_effect = self._return_version
mdisk_info = mock.MagicMock()
m_os.return_value = mdisk_info
mdisk_info.f_blocks = 34676992
mdisk_info.f_bfree = 28398016
mdisk_info.f_frsize = 4096
mdisk_info.f_bavail = 26630646
m_get_nets.return_value = {'eth1': {'network_rx': 996,
'network_tx': 418},
'eth2': {'network_rx': 848,
'network_tx': 578}}
m_load.return_value = ['0.09', '0.11', '0.10']
m_count.return_value = 5
self.udp_driver.get_subscribed_amp_compile_info.return_value = [
'keepalived', 'ipvsadm']
self.udp_driver.is_listener_running.side_effect = [True, False]
original_version = api_server.VERSION
api_server.VERSION = self.API_VERSION
expected_dict = {u'active': True,
u'api_version': self.API_VERSION,
u'cpu': {u'soft_irq': u'8336',
u'system': u'52554',
u'total': 7503411,
u'user': u'252551'},
u'disk': {u'available': 109079126016,
u'used': 25718685696},
u'haproxy_count': 5,
u'haproxy_version': self.HAPROXY_VERSION,
u'keepalived_version': self.KEEPALIVED_VERSION,
u'ipvsadm_version': self.IPVSADM_VERSION,
u'udp_listener_process_count': 1,
u'hostname': u'FAKE_HOST',
u'listeners': list(set([self.FAKE_LISTENER_ID_1,
self.FAKE_LISTENER_ID_2,
self.FAKE_LISTENER_ID_3,
self.FAKE_LISTENER_ID_4])),
u'load': [u'0.09', u'0.11', u'0.10'],
u'memory': {u'buffers': 344792,
u'cached': 4271856,
u'free': 12685624,
u'shared': 9520,
u'slab': 534384,
u'swap_used': 0,
u'total': 21692784},
u'networks': {u'eth1': {u'network_rx': 996,
u'network_tx': 418},
u'eth2': {u'network_rx': 848,
u'network_tx': 578}},
u'packages': {},
u'topology': u'SINGLE',
u'topology_status': u'OK'}
actual = self.amp_info.compile_amphora_details(self.udp_driver)
self.assertEqual(expected_dict, actual.json)
api_server.VERSION = original_version
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'is_listener_running')
def test__count_haproxy_process(self, mock_is_running):
@ -63,6 +258,29 @@ class TestAmphoraInfo(base.TestCase):
[uuidutils.generate_uuid(), uuidutils.generate_uuid()])
self.assertEqual(1, result)
def test__count_udp_listener_processes(self):
self.udp_driver.is_listener_running.side_effect = [True, False, True]
expected = 2
actual = self.amp_info._count_udp_listener_processes(
self.udp_driver, [self.FAKE_LISTENER_ID_1,
self.FAKE_LISTENER_ID_2,
self.FAKE_LISTENER_ID_3])
self.assertEqual(expected, actual)
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_version_of_installed_package')
def test__get_extend_body_from_udp_driver(self, m_get_version):
self.udp_driver.get_subscribed_amp_compile_info.return_value = [
'keepalived', 'ipvsadm']
m_get_version.side_effect = self._return_version
expected = {
"keepalived_version": self.KEEPALIVED_VERSION,
"ipvsadm_version": self.IPVSADM_VERSION
}
actual = self.amp_info._get_extend_body_from_udp_driver(
self.udp_driver)
self.assertEqual(expected, actual)
def test__get_meminfo(self):
# Known data test
meminfo = ('MemTotal: 21692784 kB\n'

View File

@ -0,0 +1,422 @@
# 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 stat
import subprocess
import flask
import mock
from werkzeug import exceptions
from oslo_utils import uuidutils
from octavia.amphorae.backends.agent.api_server import keepalivedlvs
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.tests.common import utils as test_utils
from octavia.tests.unit import base
class KeepalivedLvsTestCase(base.TestCase):
FAKE_ID = uuidutils.generate_uuid()
LISTENER_ID = 'listener-1111-1111-1111-listenerid00'
POOL_ID = 'poolpool-1111-1111-1111-poolid000000'
MEMBER_ID1 = 'memberid-1111-1111-1111-memberid1111'
MEMBER_ID2 = 'memberid-2222-2222-2222-memberid2222'
HEALTHMONITOR_ID = 'hmidhmid-1111-1111-1111-healthmonito'
NORMAL_CFG_CONTENT = (
"# Configuration for Listener %(listener_id)s\n\n"
"net_namespace haproxy-amphora\n\n"
"virtual_server 10.0.0.2 80 {\n"
" lb_algo rr\n"
" lb_kind NAT\n"
" protocol udp\n"
" delay_loop 30\n"
" delay_before_retry 31\n"
" retry 3\n\n\n"
" # Configuration for Pool %(pool_id)s\n"
" # Configuration for HealthMonitor %(hm_id)s\n"
" # Configuration for Member %(member1_id)s\n"
" real_server 10.0.0.99 82 {\n"
" weight 13\n"
" inhibit_on_failure\n"
" uthreshold 98\n"
" persistence_timeout 33\n"
" persistence_granularity 255.255.0.0\n"
" delay_before_retry 31\n"
" retry 3\n"
" MISC_CHECK {\n"
" misc_path \"/var/lib/octavia/lvs/check/"
"udp_check.sh 10.0.0.99 82\"\n"
" misc_timeout 30\n"
" misc_dynamic\n"
" }\n"
" }\n\n"
" # Configuration for Member %(member2_id)s\n"
" real_server 10.0.0.98 82 {\n"
" weight 13\n"
" inhibit_on_failure\n"
" uthreshold 98\n"
" persistence_timeout 33\n"
" persistence_granularity 255.255.0.0\n"
" delay_before_retry 31\n"
" retry 3\n"
" MISC_CHECK {\n"
" misc_path \"/var/lib/octavia/lvs/check/"
"udp_check.sh 10.0.0.98 82\"\n"
" misc_timeout 30\n"
" misc_dynamic\n"
" }\n"
" }\n\n"
"}\n\n") % {'listener_id': LISTENER_ID, 'pool_id': POOL_ID,
'hm_id': HEALTHMONITOR_ID, 'member1_id': MEMBER_ID1,
'member2_id': MEMBER_ID2}
PROC_CONTENT = (
"IP Virtual Server version 1.2.1 (size=4096)\n"
"Prot LocalAddress:Port Scheduler Flags\n"
" -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n"
"UDP 0A000002:0050 sh\n"
" -> 0A000063:0052 Masq 13 1 0\n"
" -> 0A000062:0052 Masq 13 1 0\n"
)
NORMAL_PID_CONTENT = "1988"
TEST_URL = server.PATH_PREFIX + '/listeners/%s/%s/udp_listener'
def setUp(self):
super(KeepalivedLvsTestCase, self).setUp()
self.app = flask.Flask(__name__)
self.client = self.app.test_client()
self._ctx = self.app.test_request_context()
self._ctx.push()
self.test_keepalivedlvs = keepalivedlvs.KeepalivedLvs()
self.app.add_url_rule(
rule=self.TEST_URL % ('<amphora_id>', '<listener_id>'),
view_func=(lambda amphora_id, listener_id:
self.test_keepalivedlvs.upload_udp_listener_config(
listener_id)),
methods=['PUT'])
@mock.patch('pyroute2.NetNS')
@mock.patch('shutil.copy2')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSTEMD)
@mock.patch('os.chmod')
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@mock.patch('os.remove')
@mock.patch('subprocess.check_output')
def test_upload_udp_listener_config_no_vrrp_check_dir(
self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod,
m_os_sysinit, m_copy2, mock_netns):
m_exists.side_effect = [False, False, True, True, False, False]
cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID)
m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open
with mock.patch('os.open') as m_open, mock.patch.object(os,
'fdopen',
m) as m_fdopen:
m_open.side_effect = ['TEST-WRITE-CFG',
'TEST-WRITE-SYSINIT']
res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID),
data=self.NORMAL_CFG_CONTENT)
os_mkdir_calls = [
mock.call(util.keepalived_lvs_dir()),
mock.call(util.keepalived_backend_check_script_dir())
]
m_os_mkdir.assert_has_calls(os_mkdir_calls)
m_os_chmod.assert_called_with(
util.keepalived_backend_check_script_path(), stat.S_IEXEC)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
systemd_cfg_path = util.keepalived_lvs_init_path(
consts.INIT_SYSTEMD, self.FAKE_ID)
m_open_calls = [
mock.call(cfg_path, flags, mode),
mock.call(systemd_cfg_path, flags, mode)
]
m_open.assert_has_calls(m_open_calls)
m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb')
m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w')
self.assertEqual(200, res.status_code)
@mock.patch('pyroute2.NetNS')
@mock.patch('shutil.copy2')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSTEMD)
@mock.patch('os.chmod')
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@mock.patch('os.remove')
@mock.patch('subprocess.check_output')
def test_upload_udp_listener_config_with_vrrp_check_dir(
self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod,
m_os_sysinit, m_copy2, mock_netns):
m_exists.side_effect = [False, False, True, True, True, False, False]
cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID)
m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open
with mock.patch('os.open') as m_open, mock.patch.object(os,
'fdopen',
m) as m_fdopen:
m_open.side_effect = ['TEST-WRITE-CFG',
'TEST-WRITE-SYSINIT',
'TEST-WRITE-UDP-VRRP-CHECK']
res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID),
data=self.NORMAL_CFG_CONTENT)
os_mkdir_calls = [
mock.call(util.keepalived_lvs_dir()),
mock.call(util.keepalived_backend_check_script_dir())
]
m_os_mkdir.assert_has_calls(os_mkdir_calls)
m_os_chmod.assert_called_with(
util.keepalived_backend_check_script_path(), stat.S_IEXEC)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
systemd_cfg_path = util.keepalived_lvs_init_path(
consts.INIT_SYSTEMD, self.FAKE_ID)
script_path = os.path.join(
util.keepalived_check_scripts_dir(),
keepalivedlvs.KEEPALIVED_CHECK_SCRIPT_NAME)
m_open_calls = [
mock.call(cfg_path, flags, mode),
mock.call(systemd_cfg_path, flags, mode),
mock.call(script_path, flags, stat.S_IEXEC)
]
m_open.assert_has_calls(m_open_calls)
m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb')
m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w')
m_fdopen.assert_any_call('TEST-WRITE-UDP-VRRP-CHECK', 'w')
self.assertEqual(200, res.status_code)
@mock.patch('shutil.copy2')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSTEMD)
@mock.patch('os.chmod')
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@mock.patch('os.remove')
@mock.patch('subprocess.check_output')
def test_upload_udp_listener_config_start_service_failure(
self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod,
m_os_sysinit, m_copy2):
m_exists.side_effect = [False, False, True, True, True, False]
m_check_output.side_effect = subprocess.CalledProcessError(1, 'blah!')
cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID)
m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open
with mock.patch('os.open') as m_open, mock.patch.object(os,
'fdopen',
m) as m_fdopen:
m_open.side_effect = ['TEST-WRITE-CFG',
'TEST-WRITE-SYSINIT']
res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID),
data=self.NORMAL_CFG_CONTENT)
os_mkdir_calls = [
mock.call(util.keepalived_lvs_dir()),
mock.call(util.keepalived_backend_check_script_dir())
]
m_os_mkdir.assert_has_calls(os_mkdir_calls)
m_os_chmod.assert_called_with(
util.keepalived_backend_check_script_path(), stat.S_IEXEC)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
systemd_cfg_path = util.keepalived_lvs_init_path(
consts.INIT_SYSTEMD, self.FAKE_ID)
m_open_calls = [
mock.call(cfg_path, flags, mode),
mock.call(systemd_cfg_path, flags, mode)
]
m_open.assert_has_calls(m_open_calls)
m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb')
m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w')
self.assertEqual(500, res.status_code)
@mock.patch('subprocess.check_output')
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'keepalivedlvs.KeepalivedLvs.'
'_check_udp_listener_exists')
def test_manage_udp_listener(self, mock_udp_exist, mock_check_output):
res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID,
'start')
cmd = ("/usr/sbin/service octavia-keepalivedlvs-{listener_id}"
" {action}".format(listener_id=self.FAKE_ID, action='start'))
mock_check_output.assert_called_once_with(cmd.split(),
stderr=subprocess.STDOUT)
self.assertEqual(202, res.status_code)
res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID,
'restart')
self.assertEqual(400, res.status_code)
mock_check_output.side_effect = subprocess.CalledProcessError(1,
'blah!')
res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID,
'start')
self.assertEqual(500, res.status_code)
@mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.'
'get_listener_realserver_mapping')
@mock.patch('subprocess.check_output', return_value=PROC_CONTENT)
@mock.patch('os.path.exists')
def test_get_udp_listener_status(self, m_exist, m_check_output,
mget_mapping):
mget_mapping.return_value = (
True, {'10.0.0.99:82': {'status': 'UP',
'Weight': '13',
'InActConn': '0',
'ActiveConn': '0'},
'10.0.0.98:82': {'status': 'UP',
'Weight': '13',
'InActConn': '0',
'ActiveConn': '0'}})
pid_path = ('/var/lib/octavia/lvs/octavia-'
'keepalivedlvs-%s.pid' % self.FAKE_ID)
self.useFixture(test_utils.OpenFixture(pid_path,
self.NORMAL_PID_CONTENT))
cfg_path = ('/var/lib/octavia/lvs/octavia-'
'keepalivedlvs-%s.conf' % self.FAKE_ID)
self.useFixture(test_utils.OpenFixture(cfg_path,
self.NORMAL_CFG_CONTENT))
m_exist.return_value = True
expected = {'status': 'ACTIVE',
'pools': [{'lvs': {
'members': {self.MEMBER_ID1: 'UP',
self.MEMBER_ID2: 'UP'},
'status': 'UP',
'uuid': self.POOL_ID}}],
'type': 'lvs', 'uuid': self.FAKE_ID}
res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID)
self.assertEqual(200, res.status_code)
self.assertEqual(expected, res.json)
@mock.patch('os.path.exists')
def test_get_udp_listener_status_no_exists(self, m_exist):
m_exist.return_value = False
self.assertRaises(exceptions.HTTPException,
self.test_keepalivedlvs.get_udp_listener_status,
self.FAKE_ID)
@mock.patch('os.path.exists')
def test_get_udp_listener_status_offline_status(self, m_exist):
m_exist.return_value = True
pid_path = ('/var/lib/octavia/lvs/octavia-'
'keepalivedlvs-%s.pid' % self.FAKE_ID)
self.useFixture(test_utils.OpenFixture(pid_path,
self.NORMAL_PID_CONTENT))
cfg_path = ('/var/lib/octavia/lvs/octavia-'
'keepalivedlvs-%s.conf' % self.FAKE_ID)
self.useFixture(test_utils.OpenFixture(cfg_path, 'NO VS CONFIG'))
expected = {'status': 'OFFLINE',
'type': '',
'uuid': self.FAKE_ID}
res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID)
self.assertEqual(200, res.status_code)
self.assertEqual(expected, res.json)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_udp_listeners', return_value=[LISTENER_ID])
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSTEMD)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_keepalivedlvs_pid')
@mock.patch('subprocess.check_output')
@mock.patch('os.remove')
@mock.patch('os.path.exists')
def test_delete_udp_listener(self, m_exist, m_remove, m_check_output,
mget_pid, m_init_sys, mget_udp_listeners):
m_exist.return_value = True
res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID)
cmd1 = ("/usr/sbin/service "
"octavia-keepalivedlvs-{0} stop".format(self.FAKE_ID))
cmd2 = ("systemctl disable "
"octavia-keepalivedlvs-{list}".format(list=self.FAKE_ID))
calls = [
mock.call(cmd1.split(), stderr=subprocess.STDOUT),
mock.call(cmd2.split(), stderr=subprocess.STDOUT)
]
m_check_output.assert_has_calls(calls)
self.assertEqual(200, res.status_code)
@mock.patch.object(keepalivedlvs, "webob")
@mock.patch('os.path.exists')
def test_delete_udp_listener_not_exist(self, m_exist, m_webob):
m_exist.return_value = False
self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID)
calls = [
mock.call(
json=dict(message='UDP Listener Not Found',
details="No UDP listener with UUID: "
"{0}".format(self.FAKE_ID)), status=404),
mock.call(json={'message': 'OK'})
]
m_webob.Response.assert_has_calls(calls)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_keepalivedlvs_pid')
@mock.patch('subprocess.check_output')
@mock.patch('os.path.exists')
def test_delete_udp_listener_stop_service_fail(self, m_exist,
m_check_output, mget_pid):
m_exist.return_value = True
m_check_output.side_effect = subprocess.CalledProcessError(1,
'Woops!')
res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID)
self.assertEqual(500, res.status_code)
self.assertEqual({'message': 'Error stopping keepalivedlvs',
'details': None}, res.json)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system', return_value=consts.INIT_SYSVINIT)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_keepalivedlvs_pid')
@mock.patch('subprocess.check_output')
@mock.patch('os.remove')
@mock.patch('os.path.exists')
def test_delete_udp_listener_disable_service_fail(self, m_exist, m_remove,
m_check_output, mget_pid,
m_init_sys):
m_exist.return_value = True
m_check_output.side_effect = [True,
subprocess.CalledProcessError(
1, 'Woops!')]
res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID)
self.assertEqual(500, res.status_code)
self.assertEqual({
'message': 'Error disabling '
'octavia-keepalivedlvs-%s service' % self.FAKE_ID,
'details': None}, res.json)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_os_init_system')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_keepalivedlvs_pid')
@mock.patch('subprocess.check_output')
@mock.patch('os.remove')
@mock.patch('os.path.exists')
def test_delete_udp_listener_unsupported_sysinit(self, m_exist, m_remove,
m_check_output, mget_pid,
m_init_sys):
m_exist.return_value = True
self.assertRaises(
util.UnknownInitError, self.test_keepalivedlvs.delete_udp_listener,
self.FAKE_ID)

View File

@ -88,9 +88,17 @@ class TestPlug(base.TestCase):
'details': 'VIP {vip} plugged on interface {interface}'.format(
vip=FAKE_IP_IPV4, interface='eth1')
}, status=202)
mock_nspopen.assert_called_once_with(
'amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE)
calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy', ['modprobe', 'ip_vs'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.ip_forward=1'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'],
stdout=subprocess.PIPE)]
mock_nspopen.assert_has_calls(calls, any_order=True)
@mock.patch('pyroute2.NSPopen')
@mock.patch.object(plug, "webob")
@ -116,9 +124,18 @@ class TestPlug(base.TestCase):
'details': 'VIP {vip} plugged on interface {interface}'.format(
vip=FAKE_IP_IPV6_EXPANDED, interface='eth1')
}, status=202)
mock_nspopen.assert_called_once_with(
'amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE)
calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy', ['modprobe', 'ip_vs'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w',
'net.ipv6.conf.all.forwarding=1'],
stdout=subprocess.PIPE),
mock.call('amphora-haproxy',
['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'],
stdout=subprocess.PIPE)]
mock_nspopen.assert_has_calls(calls, any_order=True)
@mock.patch.object(plug, "webob")
@mock.patch('pyroute2.IPRoute')
@ -191,7 +208,11 @@ class TestPlugNetwork(base.TestCase):
'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n'
'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n')
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n'
'post-up /sbin/iptables -t nat -A POSTROUTING -p udp -o '
'eth1234 -j MASQUERADE\n'
'post-down /sbin/iptables -t nat -D POSTROUTING -p udp -o eth1234 '
'-j MASQUERADE\n')
template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2')
text = self.test_plug._osutils._generate_network_file_text(

View File

@ -34,6 +34,8 @@ class AgentJinjaTestCase(base.TestCase):
agent_server_cert='/etc/octavia/certs/server.pem')
self.conf.config(group="amphora_agent",
agent_server_network_dir='/etc/network/interfaces.d/')
self.conf.config(group='amphora_agent',
amphora_udp_driver='keepalived_lvs'),
self.conf.config(group="haproxy_amphora",
base_cert_dir='/var/lib/octavia/certs')
self.conf.config(group="haproxy_amphora", use_upstart='True')
@ -79,7 +81,8 @@ class AgentJinjaTestCase(base.TestCase):
'agent_server_network_dir = '
'/etc/network/interfaces.d/\n'
'agent_request_read_timeout = 120\n'
'amphora_id = ' + AMP_ID)
'amphora_id = ' + AMP_ID + '\n'
'amphora_udp_driver = keepalived_lvs')
agent_cfg = ajc.build_agent_config(AMP_ID)
self.assertEqual(expected_config, agent_cfg)
@ -114,6 +117,42 @@ class AgentJinjaTestCase(base.TestCase):
'agent_server_network_file = '
'/etc/network/interfaces\n'
'agent_request_read_timeout = 120\n'
'amphora_id = ' + AMP_ID)
'amphora_id = ' + AMP_ID + '\n'
'amphora_udp_driver = keepalived_lvs')
agent_cfg = ajc.build_agent_config(AMP_ID)
self.assertEqual(expected_config, agent_cfg)
def test_build_agent_config_with_new_udp_driver(self):
ajc = agent_jinja_cfg.AgentJinjaTemplater()
self.conf.config(group='amphora_agent',
agent_server_network_file=None)
self.conf.config(group="amphora_agent",
amphora_udp_driver='new_udp_driver')
expected_config = ('\n[DEFAULT]\n'
'debug = False\n\n'
'[haproxy_amphora]\n'
'base_cert_dir = /var/lib/octavia/certs\n'
'base_path = /var/lib/octavia\n'
'bind_host = 0.0.0.0\n'
'bind_port = 9443\n'
'haproxy_cmd = /usr/sbin/haproxy\n'
'respawn_count = 2\n'
'respawn_interval = 2\n'
'use_upstart = True\n'
'user_group = nogroup\n\n'
'[health_manager]\n'
'controller_ip_port_list = 192.0.2.10:5555\n'
'heartbeat_interval = 10\n'
'heartbeat_key = TEST\n\n'
'[amphora_agent]\n'
'agent_server_ca = '
'/etc/octavia/certs/client_ca.pem\n'
'agent_server_cert = '
'/etc/octavia/certs/server.pem\n'
'agent_server_network_dir = '
'/etc/network/interfaces.d/\n'
'agent_request_read_timeout = 120\n'
'amphora_id = ' + AMP_ID + '\n'
'amphora_udp_driver = new_udp_driver')
agent_cfg = ajc.build_agent_config(AMP_ID)
self.assertEqual(expected_config, agent_cfg)

View File

@ -20,6 +20,7 @@ from oslo_utils import uuidutils
import six
from octavia.amphorae.backends.health_daemon import health_daemon
from octavia.common import constants
import octavia.tests.unit.base as base
if six.PY2:
@ -353,6 +354,65 @@ class TestHealthDaemon(base.TestCase):
self.assertEqual(msg['listeners'][LISTENER_ID1]['pools'], {})
@mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query."
"get_udp_listener_pool_status")
@mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query."
"get_udp_listeners_stats")
@mock.patch("octavia.amphorae.backends.agent.api_server.util."
"get_udp_listeners")
def test_bulid_stats_message_with_udp_listener(
self, mock_get_udp_listeners, mock_get_listener_stats,
mock_get_pool_status):
udp_listener_id1 = uuidutils.generate_uuid()
udp_listener_id2 = uuidutils.generate_uuid()
udp_listener_id3 = uuidutils.generate_uuid()
pool_id = uuidutils.generate_uuid()
member_id1 = uuidutils.generate_uuid()
member_id2 = uuidutils.generate_uuid()
mock_get_udp_listeners.return_value = [udp_listener_id1,
udp_listener_id2,
udp_listener_id3]
mock_get_listener_stats.return_value = {
udp_listener_id1: {
'status': constants.OPEN,
'stats': {'bin': 6387472, 'stot': 5, 'bout': 7490,
'ereq': 0, 'scur': 0}},
udp_listener_id3: {
'status': constants.DOWN,
'stats': {'bin': 0, 'stot': 0, 'bout': 0,
'ereq': 0, 'scur': 0}}
}
udp_pool_status = {
'lvs': {
'uuid': pool_id,
'status': constants.UP,
'members': {member_id1: constants.UP,
member_id2: constants.UP}}}
mock_get_pool_status.side_effect = (
lambda x: udp_pool_status if x == udp_listener_id1 else {})
# the first listener can get all necessary info.
# the second listener can not get listener stats, so we won't report it
# the third listener can get listener stats, but can not get pool
# status, so the result will just contain the listener status for it.
expected = {
'listeners': {
udp_listener_id1: {
'status': constants.OPEN,
'pools': {
pool_id: {
'status': constants.UP,
'members': {
member_id1: constants.UP,
member_id2: constants.UP}}},
'stats': {'conns': 0, 'totconns': 5, 'ereq': 0,
'rx': 6387472, 'tx': 7490}},
udp_listener_id3: {
'status': constants.DOWN,
'stats': {'conns': 0, 'totconns': 0, 'ereq': 0,
'rx': 0, 'tx': 0}}}, 'id': None, 'seq': mock.ANY}
msg = health_daemon.build_stats_message()
self.assertEqual(expected, msg)
class FileNotFoundError(IOError):
errno = 2

View File

@ -0,0 +1,397 @@
# 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_utils import uuidutils
from octavia.amphorae.backends.agent.api_server import util
from octavia.amphorae.backends.utils import keepalivedlvs_query as lvs_query
from octavia.common import constants
from octavia.tests.common import utils as test_utils
from octavia.tests.unit import base
# Kernal_file_sample which is in /proc/net/ip_vs
# The realservers and the listened ports are
# 10.0.0.25:2222, 10.0.0.35:3333.
# The virtual server and the listened port is
# 10.0.0.37:7777.
KERNAL_FILE_SAMPLE_V4 = (
"IP Virtual Server version 1.2.1 (size=4096)\n"
"Prot LocalAddress:Port Scheduler Flags\n"
" -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n"
"UDP 0A000025:1E61 rr\n"
" -> 0A000023:0D05 Masq 2 0 0\n"
" -> 0A000019:08AE Masq 3 0 0")
# Kernal_file_sample which is in /proc/net/ip_vs
# The realservers and the listened ports are
# [fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222,
# [fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333.
# The virtual server and the listened port is
# [fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777.
KERNAL_FILE_SAMPLE_V6 = (
"IP Virtual Server version 1.2.1 (size=4096)\n"
"Prot LocalAddress:Port Scheduler Flags\n"
" -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n"
"UDP [fd79:35e2:9963:0000:f816:3eff:fe6d:7a2a]:1E61 rr\n"
" -> [fd79:35e2:9963:0000:f816:3eff:feca:b7bf]:08AE "
"Masq 3 0 0\n"
" -> [fd79:35e2:9963:0000:f816:3eff:fe9d:94df]:0D05 "
"Masq 2 0 0")
CFG_FILE_TEMPLATE_v4 = (
"# Configuration for Listener %(listener_id)s\n\n"
"net_namespace %(ns_name)s\n\n"
"virtual_server 10.0.0.37 7777 {\n"
" lb_algo rr\n"
" lb_kind NAT\n"
" protocol udp\n\n\n"
" # Configuration for Pool %(pool_id)s\n"
" # Configuration for Member %(member_id1)s\n"
" real_server 10.0.0.25 2222 {\n"
" weight 3\n"
" inhibit_on_failure\n"
" persistence_timeout 5\n"
" persistence_granularity 255.0.0.0\n\n"
" }\n\n"
" # Configuration for Member %(member_id2)s\n"
" real_server 10.0.0.35 3333 {\n"
" weight 2\n"
" inhibit_on_failure\n"
" persistence_timeout 5\n"
" persistence_granularity 255.0.0.0\n\n"
" }\n\n"
"}")
CFG_FILE_TEMPLATE_v6 = (
"# Configuration for Listener %(listener_id)s\n\n"
"net_namespace %(ns_name)s\n\n"
"virtual_server fd79:35e2:9963:0:f816:3eff:fe6d:7a2a 7777 {\n"
" lb_algo rr\n"
" lb_kind NAT\n"
" protocol udp\n\n\n"
" # Configuration for Pool %(pool_id)s\n"
" # Configuration for Member %(member_id1)s\n"
" real_server fd79:35e2:9963:0:f816:3eff:feca:b7bf 2222 {\n"
" weight 3\n"
" inhibit_on_failure\n"
" }\n\n"
" # Configuration for Member %(member_id2)s\n"
" real_server fd79:35e2:9963:0:f816:3eff:fe9d:94df 3333 {\n"
" weight 2\n"
" inhibit_on_failure\n"
" }\n\n"
"}")
IPVSADM_OUTPUT_TEMPLATE = (
"IP Virtual Server version 1.2.1 (size=4096)\n"
"Prot LocalAddress:Port Scheduler Flags\n"
" -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n"
"UDP %(listener_ipport)s rr\n"
" -> %(member1_ipport)s Masq 3 0 0\n"
" -> %(member2_ipport)s Masq 2 0 0")
IPVSADM_STATS_OUTPUT_TEMPLATE = (
"IP Virtual Server version 1.2.1 (size=4096)\n"
"Prot LocalAddress:Port Conns InPkts OutPkts "
"InBytes OutBytes\n"
" -> RemoteAddress:Port\n"
"UDP %(listener_ipport)s 5 4264 5"
" 6387472 7490\n"
" -> %(member1_ipport)s 2 1706 2"
" 2555588 2996\n"
" -> %(member2_ipport)s 3 2558 3"
" 3831884 4494")
class LvsQueryTestCase(base.TestCase):
def setUp(self):
super(LvsQueryTestCase, self).setUp()
self.listener_id_v4 = uuidutils.generate_uuid()
self.pool_id_v4 = uuidutils.generate_uuid()
self.member_id1_v4 = uuidutils.generate_uuid()
self.member_id2_v4 = uuidutils.generate_uuid()
self.listener_id_v6 = uuidutils.generate_uuid()
self.pool_id_v6 = uuidutils.generate_uuid()
self.member_id1_v6 = uuidutils.generate_uuid()
self.member_id2_v6 = uuidutils.generate_uuid()
cfg_content_v4 = CFG_FILE_TEMPLATE_v4 % {
'listener_id': self.listener_id_v4,
'ns_name': constants.AMPHORA_NAMESPACE,
'pool_id': self.pool_id_v4,
'member_id1': self.member_id1_v4,
'member_id2': self.member_id2_v4
}
cfg_content_v6 = CFG_FILE_TEMPLATE_v6 % {
'listener_id': self.listener_id_v6,
'ns_name': constants.AMPHORA_NAMESPACE,
'pool_id': self.pool_id_v6,
'member_id1': self.member_id1_v6,
'member_id2': self.member_id2_v6
}
self.useFixture(test_utils.OpenFixture(
util.keepalived_lvs_cfg_path(self.listener_id_v4), cfg_content_v4))
self.useFixture(test_utils.OpenFixture(
util.keepalived_lvs_cfg_path(self.listener_id_v6), cfg_content_v6))
@mock.patch('subprocess.check_output')
def test_get_listener_realserver_mapping(self, mock_check_output):
# Ipv4 resolver
input_listener_ip_port = '10.0.0.37:7777'
target_ns = constants.AMPHORA_NAMESPACE
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4
result = lvs_query.get_listener_realserver_mapping(
target_ns, input_listener_ip_port)
expected = {'10.0.0.25:2222': {'status': 'UP',
'Forward': 'Masq',
'Weight': '3',
'ActiveConn': '0',
'InActConn': '0'},
'10.0.0.35:3333': {'status': 'UP',
'Forward': 'Masq',
'Weight': '2',
'ActiveConn': '0',
'InActConn': '0'}}
self.assertEqual((True, expected), result)
# Ipv6 resolver
input_listener_ip_port = '[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777'
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6
result = lvs_query.get_listener_realserver_mapping(
target_ns, input_listener_ip_port)
expected = {'[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222':
{'status': constants.UP,
'Forward': 'Masq',
'Weight': '3',
'ActiveConn': '0',
'InActConn': '0'},
'[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333':
{'status': constants.UP,
'Forward': 'Masq',
'Weight': '2',
'ActiveConn': '0',
'InActConn': '0'}}
self.assertEqual((True, expected), result)
# negetive cases
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4
for listener_ip_port in ['10.0.0.37:7776', '10.0.0.31:7777']:
result = lvs_query.get_listener_realserver_mapping(
target_ns, listener_ip_port)
self.assertEqual((False, {}), result)
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6
for listener_ip_port in [
'[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7776',
'[fd79:35e2:9973:0:f816:3eff:fe6d:7a2a]:7777']:
result = lvs_query.get_listener_realserver_mapping(
target_ns, listener_ip_port)
self.assertEqual((False, {}), result)
def test_get_udp_listener_resource_ipports_nsname(self):
# ipv4
res = lvs_query.get_udp_listener_resource_ipports_nsname(
self.listener_id_v4)
expected = {'Listener': {'id': self.listener_id_v4,
'ipport': '10.0.0.37:7777'},
'Pool': {'id': self.pool_id_v4},
'Members': [{'id': self.member_id1_v4,
'ipport': '10.0.0.25:2222'},
{'id': self.member_id2_v4,
'ipport': '10.0.0.35:3333'}]}
self.assertEqual((expected, constants.AMPHORA_NAMESPACE), res)
# ipv6
res = lvs_query.get_udp_listener_resource_ipports_nsname(
self.listener_id_v6)
expected = {'Listener': {
'id': self.listener_id_v6,
'ipport': '[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777'},
'Pool': {'id': self.pool_id_v6},
'Members': [
{'id': self.member_id1_v6,
'ipport': '[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222'},
{'id': self.member_id2_v6,
'ipport': '[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333'}]}
self.assertEqual((expected, constants.AMPHORA_NAMESPACE), res)
@mock.patch('subprocess.check_output')
def test_get_udp_listener_pool_status(self, mock_check_output):
# test with ipv4 and ipv6
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4
res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4)
expected = {
'lvs':
{'uuid': self.pool_id_v4,
'status': constants.UP,
'members': {self.member_id1_v4: constants.UP,
self.member_id2_v4: constants.UP}}}
self.assertEqual(expected, res)
mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6
res = lvs_query.get_udp_listener_pool_status(self.listener_id_v6)
expected = {
'lvs':
{'uuid': self.pool_id_v6,
'status': constants.UP,
'members': {self.member_id1_v6: constants.UP,
self.member_id2_v6: constants.UP}}}
self.assertEqual(expected, res)
@mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.'
'get_udp_listener_resource_ipports_nsname')
def test_get_udp_listener_pool_status_when_no_pool(
self, mock_get_resource_ipports):
# Just test with ipv4, ipv6 tests is same.
# the returned resource_ipport_mapping doesn't contains the 'Pool'
# resource, that means the listener doesn't have a pool resource, it
# isn't usable at this moment, then the pool status will
# return nothing.
mock_get_resource_ipports.return_value = (
{
'Listener': {
'id': self.listener_id_v4,
'ipport': '10.0.0.37:7777'}},
constants.AMPHORA_NAMESPACE)
res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4)
self.assertEqual({}, res)
@mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.'
'get_udp_listener_resource_ipports_nsname')
def test_get_udp_listener_pool_status_when_no_members(
self, mock_get_resource_ipports):
# Just test with ipv4, ipv6 tests is same.
# the returned resource_ipport_mapping doesn't contains the 'Members'
# resources, that means the pool of listener doesn't have a enabled
# pool resource, so the pool is not usable, then the pool status will
# return DOWN.
mock_get_resource_ipports.return_value = (
{
'Listener': {'id': self.listener_id_v4,
'ipport': '10.0.0.37:7777'},
'Pool': {'id': self.pool_id_v4}},
constants.AMPHORA_NAMESPACE)
res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4)
expected = {'lvs': {
'uuid': self.pool_id_v4,
'status': constants.DOWN,
'members': {}
}}
self.assertEqual(expected, res)
@mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.'
'get_listener_realserver_mapping')
def test_get_udp_listener_pool_status_when_not_get_realserver_result(
self, mock_get_mapping):
# This will hit if the kernel lvs file (/proc/net/ip_vs)
# lose its content. So at this moment, eventhough we configure the
# pool and member into udp keepalived config file, we have to set
# ths status of pool and its members to DOWN.
mock_get_mapping.return_value = (False, {})
res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4)
expected = {
'lvs':
{'uuid': self.pool_id_v4,
'status': constants.DOWN,
'members': {self.member_id1_v4: constants.DOWN,
self.member_id2_v4: constants.DOWN}}}
self.assertEqual(expected, res)
@mock.patch('subprocess.check_output')
def test_get_ipvsadm_info(self, mock_check_output):
for ip_list in [["10.0.0.37:7777", "10.0.0.25:2222", "10.0.0.35:3333"],
["[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777",
"[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222",
"[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333"]]:
mock_check_output.return_value = IPVSADM_OUTPUT_TEMPLATE % {
"listener_ipport": ip_list[0],
"member1_ipport": ip_list[1],
"member2_ipport": ip_list[2]}
res = lvs_query.get_ipvsadm_info(constants.AMPHORA_NAMESPACE)
# This expected result can referece on IPVSADM_OUTPUT_TEMPLATE,
# that means the function can get every element of the virtual
# server and the real servers.
expected = {
ip_list[0]: {
'Listener': [('Prot', 'UDP'),
('LocalAddress:Port', ip_list[0]),
('Scheduler', 'rr')],
'Members': [[('RemoteAddress:Port', ip_list[1]),
('Forward', 'Masq'), ('Weight', '3'),
('ActiveConn', '0'), ('InActConn', '0')],
[('RemoteAddress:Port', ip_list[2]),
('Forward', 'Masq'), ('Weight', '2'),
('ActiveConn', '0'), ('InActConn', '0')]]}}
self.assertEqual(expected, res)
# ipvsadm stats
mock_check_output.return_value = IPVSADM_STATS_OUTPUT_TEMPLATE % {
"listener_ipport": ip_list[0],
"member1_ipport": ip_list[1],
"member2_ipport": ip_list[2]}
res = lvs_query.get_ipvsadm_info(constants.AMPHORA_NAMESPACE,
is_stats_cmd=True)
expected = {
ip_list[0]:
{'Listener': [('Prot', 'UDP'),
('LocalAddress:Port', ip_list[0]),
('Conns', '5'),
('InPkts', '4264'),
('OutPkts', '5'),
('InBytes', '6387472'),
('OutBytes', '7490')],
'Members': [[('RemoteAddress:Port', ip_list[1]),
('Conns', '2'),
('InPkts', '1706'),
('OutPkts', '2'),
('InBytes', '2555588'),
('OutBytes', '2996')],
[('RemoteAddress:Port', ip_list[2]),
('Conns', '3'),
('InPkts', '2558'),
('OutPkts', '3'),
('InBytes', '3831884'),
('OutBytes', '4494')]]}}
self.assertEqual(expected, res)
@mock.patch('subprocess.check_output')
@mock.patch("octavia.amphorae.backends.agent.api_server.util."
"is_udp_listener_running", return_value=True)
@mock.patch("octavia.amphorae.backends.agent.api_server.util."
"get_udp_listeners")
def test_get_udp_listeners_stats(
self, mock_get_listener, mock_is_running, mock_check_output):
# The ipv6 test is same with ipv4, so just test ipv4 here
mock_get_listener.return_value = [self.listener_id_v4]
output_list = list()
output_list.append(IPVSADM_OUTPUT_TEMPLATE % {
"listener_ipport": "10.0.0.37:7777",
"member1_ipport": "10.0.0.25:2222",
"member2_ipport": "10.0.0.35:3333"})
output_list.append(IPVSADM_STATS_OUTPUT_TEMPLATE % {
"listener_ipport": "10.0.0.37:7777",
"member1_ipport": "10.0.0.25:2222",
"member2_ipport": "10.0.0.35:3333"})
mock_check_output.side_effect = output_list
res = lvs_query.get_udp_listeners_stats()
# We can check the expected result referece the stats sample,
# that means this func can compute the stats info of single listener.
expected = {self.listener_id_v4: {
'status': constants.OPEN,
'stats': {'bin': 6387472, 'stot': 5, 'bout': 7490,
'ereq': 0, 'scur': 0}}}
self.assertEqual(expected, res)
# if no udp listener need to be collected.
# Then this function will return nothing.
mock_is_running.return_value = False
res = lvs_query.get_udp_listeners_stats()
self.assertIsNone(res)

View File

@ -41,6 +41,9 @@ FAKE_UUID_1 = uuidutils.generate_uuid()
FAKE_VRRP_IP = '10.1.0.1'
FAKE_MAC_ADDRESS = '123'
FAKE_MTU = 1450
FAKE_MEMBER_IP_PORT_NAME_1 = "10.0.0.10:1003"
FAKE_MEMBER_IP_PORT_NAME_2 = "10.0.0.11:1004"
FAKE_PROTOCOL = 'test-protocol'
class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
@ -61,9 +64,16 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.cert_parser = mock.MagicMock()
self.driver.client = mock.MagicMock()
self.driver.jinja = mock.MagicMock()
self.driver.udp_jinja = mock.MagicMock()
# Build sample Listener and VIP configs
self.sl = sample_configs.sample_listener_tuple(tls=True, sni=True)
self.sl_udp = sample_configs.sample_listener_tuple(
proto=constants.PROTOCOL_UDP,
persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP,
persistence_timeout=33,
persistence_granularity='255.255.0.0',
monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT)
self.amp = self.sl.load_balancer.amphorae[0]
self.sv = sample_configs.sample_vip_tuple()
self.lb = self.sl.load_balancer
@ -192,7 +202,21 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.amp, self.sl.id, 'fake_config')
# start should be called once
self.driver.client.reload_listener.assert_called_once_with(
self.amp, self.sl.id)
self.amp, self.sl.id, self.sl.protocol)
def test_udp_update(self):
self.driver.udp_jinja.build_config.side_effect = ['fake_udp_config']
# Execute driver method
self.driver.update(self.sl_udp, self.sv)
# upload only one config file
self.driver.client.upload_udp_config.assert_called_once_with(
self.amp, self.sl_udp.id, 'fake_udp_config')
# start should be called once
self.driver.client.reload_listener.assert_called_once_with(
self.amp, self.sl_udp.id, self.sl_udp.protocol)
def test_upload_cert_amp(self):
self.driver.upload_cert_amp(self.amp, six.b('test'))
@ -200,10 +224,18 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.amp, six.b('test'))
def test_stop(self):
self.driver.client.stop_listener.__name__ = 'stop_listener'
# Execute driver method
self.driver.stop(self.sl, self.sv)
self.driver.client.stop_listener.assert_called_once_with(
self.amp, self.sl.id)
self.amp, self.sl.id, self.sl.protocol)
def test_udp_stop(self):
self.driver.client.stop_listener.__name__ = 'stop_listener'
# Execute driver method - UDP case
self.driver.stop(self.sl_udp, self.sv)
self.driver.client.stop_listener.assert_called_once_with(
self.amp, self.sl_udp.id, self.sl_udp.protocol)
def test_start(self):
amp1 = mock.MagicMock()
@ -212,28 +244,46 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
listener = mock.MagicMock()
listener.id = uuidutils.generate_uuid()
listener.load_balancer.amphorae = [amp1, amp2]
listener.protocol = 'listener_protocol'
self.driver.client.start_listener.__name__ = 'start_listener'
# Execute driver method
self.driver.start(listener, self.sv)
self.driver.client.start_listener.assert_called_once_with(
amp1, listener.id)
amp1, listener.id, 'listener_protocol')
def test_start_with_amphora(self):
# Execute driver method
amp = mock.MagicMock()
self.driver.client.start_listener.__name__ = 'start_listener'
self.driver.start(self.sl, self.sv, self.amp)
self.driver.client.start_listener.assert_called_once_with(
self.amp, self.sl.id)
self.amp, self.sl.id, self.sl.protocol)
self.driver.client.start_listener.reset_mock()
amp.status = constants.DELETED
self.driver.start(self.sl, self.sv, amp)
self.driver.client.start_listener.assert_not_called()
def test_udp_start(self):
self.driver.client.start_listener.__name__ = 'start_listener'
# Execute driver method
self.driver.start(self.sl_udp, self.sv)
self.driver.client.start_listener.assert_called_once_with(
self.amp, self.sl_udp.id, self.sl_udp.protocol)
def test_delete(self):
self.driver.client.delete_listener.__name__ = 'delete_listener'
# Execute driver method
self.driver.delete(self.sl, self.sv)
self.driver.client.delete_listener.assert_called_once_with(
self.amp, self.sl.id)
self.amp, self.sl.id, self.sl.protocol)
def test_udp_delete(self):
self.driver.client.delete_listener.__name__ = 'delete_listener'
# Execute driver method
self.driver.delete(self.sl_udp, self.sv)
self.driver.client.delete_listener.assert_called_once_with(
self.amp, self.sl_udp.id, self.sl_udp.protocol)
def test_get_info(self):
self.driver.client.get_info.return_value = 'FAKE_INFO'
@ -482,9 +532,31 @@ class TestAmphoraAPIClientTest(base.TestCase):
m.get("{base}/listeners/{listener_id}".format(
base=self.base_url, listener_id=FAKE_UUID_1),
json=listener)
status = self.driver.get_listener_status(self.amp, FAKE_UUID_1)
status = self.driver.get_listener_status(self.amp, FAKE_UUID_1,
protocol='TCP')
self.assertEqual(listener, status)
@requests_mock.mock()
def test_get_udp_listener_status(self, m):
udp_listener = {"status": "ACTIVE", "type": "lvs",
"uuid": FAKE_UUID_1,
"pools": [{
"UDP-Listener-%s-pool" % FAKE_UUID_1:
{
"status": "UP",
"members": [
{FAKE_MEMBER_IP_PORT_NAME_1: "DOWN"},
{FAKE_MEMBER_IP_PORT_NAME_2: "ACTIVE"},
]
}
}]}
m.get("{base}/listeners/{listener_id}".format(
base=self.base_url, listener_id=FAKE_UUID_1),
json=udp_listener)
status = self.driver.get_listener_status(self.amp, FAKE_UUID_1,
protocol='UDP')
self.assertEqual(udp_listener, status)
@requests_mock.mock()
def test_get_listener_status_unauthorized(self, m):
m.get("{base}/listeners/{listener_id}".format(
@ -526,7 +598,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
def test_start_listener(self, m):
m.put("{base}/listeners/{listener_id}/start".format(
base=self.base_url, listener_id=FAKE_UUID_1))
self.driver.start_listener(self.amp, FAKE_UUID_1)
self.driver.start_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
self.assertTrue(m.called)
@requests_mock.mock()
@ -536,7 +608,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
status_code=404,
headers={'content-type': 'application/json'})
self.assertRaises(exc.NotFound, self.driver.start_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_start_listener_unauthorized(self, m):
@ -544,7 +616,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=401)
self.assertRaises(exc.Unauthorized, self.driver.start_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_start_listener_server_error(self, m):
@ -552,7 +624,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=500)
self.assertRaises(exc.InternalServerError, self.driver.start_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_start_listener_service_unavailable(self, m):
@ -560,13 +632,13 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=503)
self.assertRaises(exc.ServiceUnavailable, self.driver.start_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_stop_listener(self, m):
m.put("{base}/listeners/{listener_id}/stop".format(
base=self.base_url, listener_id=FAKE_UUID_1))
self.driver.stop_listener(self.amp, FAKE_UUID_1)
self.driver.stop_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
self.assertTrue(m.called)
@requests_mock.mock()
@ -576,7 +648,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
status_code=404,
headers={'content-type': 'application/json'})
self.assertRaises(exc.NotFound, self.driver.stop_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_stop_listener_unauthorized(self, m):
@ -584,7 +656,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=401)
self.assertRaises(exc.Unauthorized, self.driver.stop_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_stop_listener_server_error(self, m):
@ -592,7 +664,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=500)
self.assertRaises(exc.InternalServerError, self.driver.stop_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_stop_listener_service_unavailable(self, m):
@ -600,13 +672,13 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=503)
self.assertRaises(exc.ServiceUnavailable, self.driver.stop_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_delete_listener(self, m):
m.delete("{base}/listeners/{listener_id}".format(
base=self.base_url, listener_id=FAKE_UUID_1), json={})
self.driver.delete_listener(self.amp, FAKE_UUID_1)
self.driver.delete_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
self.assertTrue(m.called)
@requests_mock.mock()
@ -615,7 +687,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=404,
headers={'content-type': 'application/json'})
self.driver.delete_listener(self.amp, FAKE_UUID_1)
self.driver.delete_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
self.assertTrue(m.called)
@requests_mock.mock()
@ -624,7 +696,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=401)
self.assertRaises(exc.Unauthorized, self.driver.delete_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_delete_listener_server_error(self, m):
@ -632,7 +704,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=500)
self.assertRaises(exc.InternalServerError, self.driver.delete_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_delete_listener_service_unavailable(self, m):
@ -640,7 +712,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
base=self.base_url, listener_id=FAKE_UUID_1),
status_code=503)
self.assertRaises(exc.ServiceUnavailable, self.driver.delete_listener,
self.amp, FAKE_UUID_1)
self.amp, FAKE_UUID_1, FAKE_PROTOCOL)
@requests_mock.mock()
def test_upload_cert_pem(self, m):
@ -875,6 +947,68 @@ class TestAmphoraAPIClientTest(base.TestCase):
self.assertRaises(exc.ServiceUnavailable, self.driver.upload_config,
self.amp, FAKE_UUID_1, config)
@requests_mock.mock()
def test_upload_udp_config(self, m):
config = {"name": "fake_config"}
m.put(
"{base}/listeners/"
"{amphora_id}/{listener_id}/udp_listener".format(
amphora_id=self.amp.id, base=self.base_url,
listener_id=FAKE_UUID_1),
json=config)
self.driver.upload_udp_config(self.amp, FAKE_UUID_1, config)
self.assertTrue(m.called)
@requests_mock.mock()
def test_upload_udp_invalid_config(self, m):
config = '{"name": "bad_config"}'
m.put(
"{base}/listeners/"
"{amphora_id}/{listener_id}/udp_listener".format(
amphora_id=self.amp.id, base=self.base_url,
listener_id=FAKE_UUID_1),
status_code=400)
self.assertRaises(exc.InvalidRequest, self.driver.upload_udp_config,
self.amp, FAKE_UUID_1, config)
@requests_mock.mock()
def test_upload_udp_config_unauthorized(self, m):
config = '{"name": "bad_config"}'
m.put(
"{base}/listeners/"
"{amphora_id}/{listener_id}/udp_listener".format(
amphora_id=self.amp.id, base=self.base_url,
listener_id=FAKE_UUID_1),
status_code=401)
self.assertRaises(exc.Unauthorized, self.driver.upload_udp_config,
self.amp, FAKE_UUID_1, config)
@requests_mock.mock()
def test_upload_udp_config_server_error(self, m):
config = '{"name": "bad_config"}'
m.put(
"{base}/listeners/"
"{amphora_id}/{listener_id}/udp_listener".format(
amphora_id=self.amp.id, base=self.base_url,
listener_id=FAKE_UUID_1),
status_code=500)
self.assertRaises(exc.InternalServerError,
self.driver.upload_udp_config,
self.amp, FAKE_UUID_1, config)
@requests_mock.mock()
def test_upload_udp_config_service_unavailable(self, m):
config = '{"name": "bad_config"}'
m.put(
"{base}/listeners/"
"{amphora_id}/{listener_id}/udp_listener".format(
amphora_id=self.amp.id, base=self.base_url,
listener_id=FAKE_UUID_1),
status_code=503)
self.assertRaises(exc.ServiceUnavailable,
self.driver.upload_udp_config,
self.amp, FAKE_UUID_1, config)
@requests_mock.mock()
def test_plug_vip(self, m):
m.post("{base}/plug/vip/{vip}".format(

View File

@ -1079,6 +1079,57 @@ class TestUpdateHealthDb(base.TestCase):
self.mock_session(), mock_lb.id,
operating_status='ONLINE')
def test_update_health_forbid_to_stale_udp_listener_amphora(self):
health = {
"id": self.FAKE_UUID_1,
"listeners": {},
"recv_time": time.time()
}
mock_lb = mock.Mock()
mock_lb.id = self.FAKE_UUID_1
mock_lb.pools = []
mock_lb.listeners = []
mock_lb.provisioning_status = constants.ACTIVE
mock_lb.operating_status = 'blah'
# The default pool of udp listener1 has no enabled member
mock_member1 = mock.Mock()
mock_member1.id = 'member-id-1'
mock_member1.enabled = False
mock_pool1 = mock.Mock()
mock_pool1.id = "pool-id-1"
mock_pool1.members = [mock_member1]
mock_listener1 = mock.Mock()
mock_listener1.id = 'listener-id-1'
mock_listener1.default_pool = mock_pool1
mock_listener1.protocol = constants.PROTOCOL_UDP
# The default pool of udp listener2 has no member
mock_pool2 = mock.Mock()
mock_pool2.id = "pool-id-2"
mock_pool2.members = []
mock_listener2 = mock.Mock()
mock_listener2.id = 'listener-id-2'
mock_listener2.default_pool = mock_pool2
mock_listener2.protocol = constants.PROTOCOL_UDP
# The udp listener3 has no default_pool
mock_listener3 = mock.Mock()
mock_listener3.id = 'listener-id-3'
mock_listener3.default_pool = None
mock_listener3.protocol = constants.PROTOCOL_UDP
mock_lb.listeners.extend([mock_listener1, mock_listener2,
mock_listener3])
mock_lb.pools.extend([mock_pool1, mock_pool2])
self.amphora_repo.get_lb_for_amphora.return_value = mock_lb
self.hm.update_health(health, '192.0.2.1')
self.assertTrue(self.amphora_repo.get_lb_for_amphora.called)
self.assertTrue(self.loadbalancer_repo.update.called)
self.assertTrue(self.amphora_health_repo.replace.called)
class TestUpdateStatsDb(base.TestCase):

View File

@ -639,8 +639,12 @@ class TestAllowedAddressPairsDriver(base.TestCase):
server=t_constants.MOCK_COMPUTE_ID, port_id=port2.get('id'))
def test_update_vip(self):
listeners = [data_models.Listener(protocol_port=80, peer_port=1024),
data_models.Listener(protocol_port=443, peer_port=1025)]
listeners = [data_models.Listener(protocol_port=80, peer_port=1024,
protocol=constants.PROTOCOL_TCP),
data_models.Listener(protocol_port=443, peer_port=1025,
protocol=constants.PROTOCOL_TCP),
data_models.Listener(protocol_port=50, peer_port=1026,
protocol=constants.PROTOCOL_UDP)]
vip = data_models.Vip(ip_address='10.0.0.2')
lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip)
list_sec_grps = self.driver.neutron_client.list_security_groups
@ -661,17 +665,27 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'security_group_rule': {
'security_group_id': 'secgrp-1',
'direction': 'ingress',
'protocol': 'TCP',
'protocol': 'tcp',
'port_range_min': 1024,
'port_range_max': 1024,
'ethertype': 'IPv4'
}
}
expected_create_rule_udp_peer = {
'security_group_rule': {
'security_group_id': 'secgrp-1',
'direction': 'ingress',
'protocol': 'tcp',
'port_range_min': 1026,
'port_range_max': 1026,
'ethertype': 'IPv4'
}
}
expected_create_rule_2 = {
'security_group_rule': {
'security_group_id': 'secgrp-1',
'direction': 'ingress',
'protocol': 'TCP',
'protocol': 'tcp',
'port_range_min': 1025,
'port_range_max': 1025,
'ethertype': 'IPv4'
@ -681,20 +695,39 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'security_group_rule': {
'security_group_id': 'secgrp-1',
'direction': 'ingress',
'protocol': 'TCP',
'protocol': 'tcp',
'port_range_min': 443,
'port_range_max': 443,
'ethertype': 'IPv4'
}
}
expected_create_rule_udp = {
'security_group_rule': {
'security_group_id': 'secgrp-1',
'direction': 'ingress',
'protocol': 'udp',
'port_range_min': 50,
'port_range_max': 50,
'ethertype': 'IPv4'
}
}
create_rule.assert_has_calls([mock.call(expected_create_rule_1),
mock.call(expected_create_rule_udp_peer),
mock.call(expected_create_rule_2),
mock.call(expected_create_rule_3)])
mock.call(expected_create_rule_3),
mock.call(expected_create_rule_udp)],
any_order=True)
def test_update_vip_when_listener_deleted(self):
listeners = [data_models.Listener(protocol_port=80),
listeners = [data_models.Listener(protocol_port=80,
protocol=constants.PROTOCOL_TCP),
data_models.Listener(
protocol_port=443,
protocol=constants.PROTOCOL_TCP,
provisioning_status=constants.PENDING_DELETE),
data_models.Listener(
protocol_port=50, protocol=constants.PROTOCOL_UDP,
provisioning_status=constants.PENDING_DELETE)]
vip = data_models.Vip(ip_address='10.0.0.2')
lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip)
@ -703,7 +736,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
fake_rules = {
'security_group_rules': [
{'id': 'rule-80', 'port_range_max': 80, 'protocol': 'tcp'},
{'id': 'rule-22', 'port_range_max': 443, 'protocol': 'tcp'}
{'id': 'rule-22', 'port_range_max': 443, 'protocol': 'tcp'},
{'id': 'rule-udp-50', 'port_range_max': 50, 'protocol': 'tcp'}
]
}
list_rules = self.driver.neutron_client.list_security_group_rules
@ -711,7 +745,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
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('rule-22')
delete_rule.assert_has_calls(
[mock.call('rule-22'), mock.call('rule-udp-50')])
self.assertTrue(create_rule.called)
def test_update_vip_when_no_listeners(self):

View File

@ -68,6 +68,8 @@ octavia.amphora.health_update_drivers =
octavia.amphora.stats_update_drivers =
stats_logger = octavia.controller.healthmanager.health_drivers.update_logging:StatsUpdateLogger
stats_db = octavia.controller.healthmanager.health_drivers.update_db:UpdateStatsDb
octavia.amphora.udp_api_server =
keepalived_lvs = octavia.amphorae.backends.agent.api_server.keepalivedlvs:KeepalivedLvs
octavia.controller.queues =
noop_event_streamer = octavia.controller.queue.event_queue:EventStreamerNoop
queue_event_streamer = octavia.controller.queue.event_queue:EventStreamerNeutron