Allow choice of upstart or sysvinit

upstart was hardcoded as the means of start, stopping and reloading
of haproxy. Allow for sysvinit scripts and paths to handle haproxy.

This patch provides a configuration option to switch
between the defaultl upstart init scripts or sysvinit.

Change-Id: I9efe51c5a08d8e2268150d69ac25725c708dfb8e
This commit is contained in:
ptoohill1 2015-10-07 19:35:32 -05:00
parent 9d06f43a1a
commit 8e31a1d044
10 changed files with 980 additions and 13 deletions

View File

@ -112,6 +112,7 @@
# respawn_interval = 2 # respawn_interval = 2
# Change for production to a ram drive # Change for production to a ram drive
# haproxy_cert_dir = /tmp # haproxy_cert_dir = /tmp
# use_upstart = True
# Maximum number of entries that can fit in the stick table. # Maximum number of entries that can fit in the stick table.
# The size supports "k", "m", "g" suffixes. # The size supports "k", "m", "g" suffixes.

View File

@ -55,5 +55,6 @@ class AgentJinjaTemplater(object):
'haproxy_cmd': CONF.haproxy_amphora.haproxy_cmd, 'haproxy_cmd': CONF.haproxy_amphora.haproxy_cmd,
'heartbeat_interval': CONF.health_manager.heartbeat_interval, 'heartbeat_interval': CONF.health_manager.heartbeat_interval,
'heartbeat_key': CONF.health_manager.heartbeat_key, 'heartbeat_key': CONF.health_manager.heartbeat_key,
'use_upstart': CONF.haproxy_amphora.use_upstart,
'respawn_count': CONF.haproxy_amphora.respawn_count, 'respawn_count': CONF.haproxy_amphora.respawn_count,
'respawn_interval': CONF.haproxy_amphora.respawn_interval}) 'respawn_interval': CONF.haproxy_amphora.respawn_interval})

View File

@ -32,11 +32,16 @@ from octavia.common import utils as octavia_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
BUFFER = 100 BUFFER = 100
HAPROXY_CONF = 'haproxy.conf.j2'
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader( UPSTART_CONF = 'upstart.conf.j2'
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) SYSVINIT_CONF = 'sysvinit.conf.j2'
template = j2_env.get_template(HAPROXY_CONF)
JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(
os.path.realpath(__file__)
) + consts.AGENT_API_TEMPLATES))
UPSTART_TEMPLATE = JINJA_ENV.get_template(UPSTART_CONF)
SYSVINIT_TEMPLATE = JINJA_ENV.get_template(SYSVINIT_CONF)
class ParsingError(Exception): class ParsingError(Exception):
@ -115,8 +120,10 @@ def upload_haproxy_config(amphora_id, listener_id):
# file ok - move it # file ok - move it
os.rename(name, util.config_path(listener_id)) os.rename(name, util.config_path(listener_id))
if not os.path.exists(util.upstart_path(listener_id)): use_upstart = util.CONF.haproxy_amphora.use_upstart
with open(util.upstart_path(listener_id), 'w') as text_file: if not os.path.exists(util.init_path(listener_id)):
with open(util.init_path(listener_id), 'w') as text_file:
template = UPSTART_TEMPLATE if use_upstart else SYSVINIT_TEMPLATE
text = template.render( text = template.render(
peer_name=peer_name, peer_name=peer_name,
haproxy_pid=util.pid_path(listener_id), haproxy_pid=util.pid_path(listener_id),
@ -127,6 +134,22 @@ def upload_haproxy_config(amphora_id, listener_id):
) )
text_file.write(text) text_file.write(text)
if not use_upstart:
# make init.d script executable
file = util.init_path(listener_id)
permcmd = ("chmod 755 {file}".format(file=file))
insrvcmd = ("insserv {file}".format(file=file))
try:
subprocess.check_output(permcmd.split(), stderr=subprocess.STDOUT)
subprocess.check_output(insrvcmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.debug("Failed to make %(file)s executable: %(err)s",
{'file': file, 'err': e})
return flask.make_response(flask.jsonify(dict(
message="Error making file {0} executable".format(file),
details=e.output)), 500)
res = flask.make_response(flask.jsonify({ res = flask.make_response(flask.jsonify({
'message': 'OK'}), 202) 'message': 'OK'}), 202)
res.headers['ETag'] = stream.get_md5() res.headers['ETag'] = stream.get_md5()
@ -203,10 +226,10 @@ def delete_listener(listener_id):
except Exception: except Exception:
pass pass
# delete the directory + upstart script for that listener # delete the directory + init script for that listener
shutil.rmtree(util.haproxy_dir(listener_id)) shutil.rmtree(util.haproxy_dir(listener_id))
if os.path.exists(util.upstart_path(listener_id)): if os.path.exists(util.init_path(listener_id)):
os.remove(util.upstart_path(listener_id)) os.remove(util.init_path(listener_id))
return flask.jsonify({'message': 'OK'}) return flask.jsonify({'message': 'OK'})

View File

@ -0,0 +1,217 @@
{#
# Copyright 2015 Rackspace.
#
# 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.
#
# Inspired by https://gist.github.com/gfrey/8472007
#}
#!/bin/sh
### BEGIN INIT INFO
# Provides: octavia-amp-{{ haproxy_pid }}
# Required-Start: $local_fs $network
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: fast and reliable load balancing reverse proxy
# Description: This file should be used to start and stop haproxy.
### END INIT INFO
PATH=/sbin:/usr/sbin:/bin:/usr/bin
PIDFILE={{ haproxy_pid }}
CONFIG={{ haproxy_cfg }}
HAPROXY={{ haproxy_cmd }}
EXTRAOPTS=
ENABLED=1
test -x $HAPROXY || exit 0
test -f "$CONFIG" || exit 0
if [ -e /etc/default/haproxy ]; then
. /etc/default/haproxy
fi
test "$ENABLED" != "0" || exit 0
[ -f /etc/default/rcS ] && . /etc/default/rcS
. /lib/lsb/init-functions
haproxy_start()
{
start-stop-daemon --start --pidfile "$PIDFILE" \
--exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \
$EXTRAOPTS || return 2
return 0
}
haproxy_stop()
{
if [ ! -f $PIDFILE ] ; then
# This is a success according to LSB
return 0
fi
for pid in $(cat $PIDFILE) ; do
/bin/kill $pid || return 4
done
rm -f $PIDFILE
return 0
}
haproxy_reload()
{
$HAPROXY -f "$CONFIG" -p $PIDFILE -D $EXTRAOPTS -sf $(cat $PIDFILE) \
|| return 2
return 0
}
haproxy_checkconf()
{
rcode=0
$HAPROXY -c -f "$CONFIG"
if [ $? -ne 0 ]; then
rcode=1
fi
return $rcode
}
haproxy_status()
{
if [ ! -f $PIDFILE ] ; then
# program not running
return 3
fi
for pid in $(cat $PIDFILE) ; do
if ! ps --no-headers p "$pid" | grep haproxy > /dev/null ; then
# program running, bogus pidfile
return 1
fi
done
return 0
}
case "$1" in
checkconf)
haproxy_checkconf
exit $?
;;
start)
log_daemon_msg "Starting haproxy" "haproxy"
haproxy_start
ret=$?
case "$ret" in
0)
log_end_msg 0
;;
1)
log_end_msg 1
echo "pid file '$PIDFILE' found, haproxy not started."
;;
2)
log_end_msg 1
;;
esac
exit $ret
;;
stop)
log_daemon_msg "Stopping haproxy" "haproxy"
haproxy_stop
ret=$?
case "$ret" in
0|1)
log_end_msg 0
;;
2)
log_end_msg 1
;;
esac
exit $ret
;;
reload|force-reload)
echo "Checking HAProxy configuration first"
haproxy_checkconf
case "$?" in
0)
echo "Everything looks fine"
;;
1)
echo "Errors..."
exit 1
;;
esac
log_daemon_msg "Reloading haproxy" "haproxy"
haproxy_reload
case "$?" in
0|1)
log_end_msg 0
;;
2)
log_end_msg 1
;;
esac
;;
restart)
echo "Checking HAProxy configuration first"
haproxy_checkconf
case "$?" in
0)
echo "Everything looks fine"
;;
1)
echo "Errors..."
exit 1
;;
esac
log_daemon_msg "Restarting haproxy" "haproxy"
haproxy_stop
haproxy_start
case "$?" in
0)
log_end_msg 0
;;
1)
log_end_msg 1
;;
2)
log_end_msg 1
;;
esac
;;
status)
haproxy_status
ret=$?
case "$ret" in
0)
echo "haproxy is running."
;;
1)
echo "haproxy dead, but $PIDFILE exists."
;;
*)
echo "haproxy not running."
;;
esac
exit $ret
;;
*)
echo "Usage: /etc/init.d/haproxy {start|stop|reload|restart|status|checkconf}"
exit 2
;;
esac
:

View File

@ -20,12 +20,17 @@ from oslo_config import cfg
CONF = cfg.CONF CONF = cfg.CONF
CONF.import_group('amphora_agent', 'octavia.common.config') CONF.import_group('amphora_agent', 'octavia.common.config')
CONF.import_group('haproxy_amphora', 'octavia.common.config') CONF.import_group('haproxy_amphora', 'octavia.common.config')
UPSTART_DIR = '/etc/init' UPSTART_DIR = '/etc/init'
KEEPALIVED_INIT_DIR = '/etc/init.d' KEEPALIVED_INIT_DIR = '/etc/init.d'
SYSVINIT_DIR = '/etc/init.d'
def upstart_path(listener_id): def init_path(listener_id):
return os.path.join(UPSTART_DIR, ('haproxy-{0}.conf'.format(listener_id))) use_upstart = CONF.haproxy_amphora.use_upstart
hconf = 'haproxy-{0}.conf' if use_upstart else 'haproxy-{0}'
idir = UPSTART_DIR if use_upstart else SYSVINIT_DIR
return os.path.join(idir, hconf.format(listener_id))
def haproxy_dir(listener_id): def haproxy_dir(listener_id):

View File

@ -23,6 +23,7 @@ bind_port = {{ bind_port }}
haproxy_cmd = {{ haproxy_cmd }} haproxy_cmd = {{ haproxy_cmd }}
respawn_count = {{ respawn_count }} respawn_count = {{ respawn_count }}
respawn_interval = {{ respawn_interval }} respawn_interval = {{ respawn_interval }}
use_upstart = {{ use_upstart }}
[health_manager] [health_manager]
controller_ip_port_list = {{ controller_list|join(', ') }} controller_ip_port_list = {{ controller_list|join(', ') }}

View File

@ -200,6 +200,8 @@ haproxy_amphora_opts = [
help=_("The client certificate to talk to the agent")), help=_("The client certificate to talk to the agent")),
cfg.StrOpt('server_ca', default='/etc/octavia/certs/server_ca.pem', cfg.StrOpt('server_ca', default='/etc/octavia/certs/server_ca.pem',
help=_("The ca which signed the server certificates")), help=_("The ca which signed the server certificates")),
cfg.BoolOpt('use_upstart', default=True,
help=_("If False, use sysvinit.")),
] ]
controller_worker_opts = [ controller_worker_opts = [

View File

@ -0,0 +1,713 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import hashlib
import json
import subprocess
import mock
import netifaces
from oslo_config import cfg
import six
from octavia.amphorae.backends.agent import api_server
from octavia.amphorae.backends.agent.api_server import certificate_update
from octavia.amphorae.backends.agent.api_server import server
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants as consts
from octavia.common import utils as octavia_utils
import octavia.tests.unit.base as base
RANDOM_ERROR = 'random error'
OK = dict(message='OK')
if six.PY2:
import __builtin__ as builtins
else:
import builtins
class ServerTestCase(base.TestCase):
app = None
def setUp(self):
cfg.CONF.set_override('use_upstart', False, group='haproxy_amphora')
self.app = server.app.test_client()
super(ServerTestCase, self).setUp()
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@mock.patch('os.rename')
@mock.patch('subprocess.check_output')
@mock.patch('os.remove')
def test_haproxy(self, mock_remove, mock_subprocess, mock_rename,
mock_makedirs, mock_exists):
mock_exists.return_value = True
m = mock.mock_open()
# happy case init file exists
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/amp_123/123/haproxy',
data='test')
self.assertEqual(202, rv.status_code)
m.assert_called_once_with(
'/var/lib/octavia/123/haproxy.cfg.new', 'w')
handle = m()
handle.write.assert_called_once_with(six.b('test'))
calls = [
mock.call("haproxy -c -L {peer} -f {config_file}".format(
config_file='/var/lib/octavia/123/haproxy.cfg.new',
peer=(octavia_utils.base64_sha1_string('amp_123')
.rstrip('='))).split(), stderr=-2),
mock.call(['chmod', '755', '/etc/init.d/haproxy-123'],
stderr=-2),
mock.call(['insserv', '/etc/init.d/haproxy-123'], stderr=-2)
]
mock_subprocess.assert_has_calls(calls)
mock_rename.assert_called_once_with(
'/var/lib/octavia/123/haproxy.cfg.new',
'/var/lib/octavia/123/haproxy.cfg')
# exception writing
m = mock.mock_open()
m.side_effect = IOError() # open crashes
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/amp_123/123/haproxy',
data='test')
self.assertEqual(500, rv.status_code)
# check if files get created
mock_exists.return_value = False
m = mock.mock_open()
# happy case init file exists
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/amp_123/123/haproxy',
data='test')
self.assertEqual(202, rv.status_code)
m.assert_any_call('/var/lib/octavia/123/haproxy.cfg.new', 'w')
m.assert_any_call(util.SYSVINIT_DIR + '/haproxy-123', 'w')
handle = m()
handle.write.assert_any_call(six.b('test'))
# skip the template stuff
mock_makedirs.assert_called_with('/var/lib/octavia/123')
# unhappy case haproxy check fails
mock_exists.return_value = True
mock_subprocess.side_effect = [subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)]
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/amp_123/123/haproxy',
data='test')
self.assertEqual(400, rv.status_code)
self.assertEqual(
{'message': 'Invalid request', u'details': u'random error'},
json.loads(rv.data.decode('utf-8')))
m.assert_called_with('/var/lib/octavia/123/haproxy.cfg.new', 'w')
handle = m()
handle.write.assert_called_with(six.b('test'))
mock_subprocess.assert_called_with(
"haproxy -c -L {peer} -f {config_file}".format(
config_file='/var/lib/octavia/123/haproxy.cfg.new',
peer=(octavia_utils.
base64_sha1_string('amp_123').rstrip('='))).split(),
stderr=-2)
mock_remove.assert_called_once_with(
'/var/lib/octavia/123/haproxy.cfg.new')
@mock.patch('os.path.exists')
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
'vrrp_check_script_update')
@mock.patch('subprocess.check_output')
def test_start(self, mock_subprocess, mock_vrrp, mock_exists):
rv = self.app.put('/' + api_server.VERSION + '/listeners/123/error')
self.assertEqual(400, rv.status_code)
self.assertEqual(
{'message': 'Invalid Request',
'details': 'Unknown action: error', },
json.loads(rv.data.decode('utf-8')))
mock_exists.return_value = False
rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start')
self.assertEqual(404, rv.status_code)
self.assertEqual(
{'message': 'Listener Not Found',
'details': 'No listener with UUID: 123'},
json.loads(rv.data.decode('utf-8')))
mock_exists.assert_called_with('/var/lib/octavia/123/haproxy.cfg')
mock_exists.return_value = True
rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start')
self.assertEqual(202, rv.status_code)
self.assertEqual(
{'message': 'OK',
'details': 'Configuration file is valid\nhaproxy daemon for'
+ ' 123 started'},
json.loads(rv.data.decode('utf-8')))
mock_subprocess.assert_called_with(
['/usr/sbin/service', 'haproxy-123', 'start'], stderr=-2)
mock_exists.return_value = True
mock_subprocess.side_effect = subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)
rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start')
self.assertEqual(500, rv.status_code)
self.assertEqual(
{
'message': 'Error starting haproxy',
'details': RANDOM_ERROR,
}, json.loads(rv.data.decode('utf-8')))
mock_subprocess.assert_called_with(
['/usr/sbin/service', 'haproxy-123', 'start'], stderr=-2)
@mock.patch('socket.gethostname')
@mock.patch('subprocess.check_output')
def test_info(self, mock_subbprocess, mock_hostname):
mock_hostname.side_effect = ['test-host']
mock_subbprocess.side_effect = [
"""Package: haproxy
Status: install ok installed
Priority: optional
Section: net
Installed-Size: 803
Maintainer: Ubuntu Developers
Architecture: amd64
Version: 1.4.24-2
"""]
rv = self.app.get('/' + api_server.VERSION + '/info')
self.assertEqual(200, rv.status_code)
self.assertEqual(dict(
api_version='0.5',
haproxy_version='1.4.24-2',
hostname='test-host'),
json.loads(rv.data.decode('utf-8')))
@mock.patch('os.path.exists')
@mock.patch('subprocess.check_output')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.' +
'get_haproxy_pid')
@mock.patch('shutil.rmtree')
@mock.patch('os.remove')
def test_delete_listener(self, mock_remove, mock_rmtree, mock_pid,
mock_check_output, mock_exists):
mock_exists.return_value = False
rv = self.app.delete('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(404, rv.status_code)
self.assertEqual(
{'message': 'Listener Not Found',
'details': 'No listener with UUID: 123'},
json.loads(rv.data.decode('utf-8')))
mock_exists.assert_called_with('/var/lib/octavia/123/haproxy.cfg')
# service is stopped + no init script
mock_exists.side_effect = [True, False, False]
rv = self.app.delete('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(200, rv.status_code)
self.assertEqual({u'message': u'OK'},
json.loads(rv.data.decode('utf-8')))
mock_rmtree.assert_called_with('/var/lib/octavia/123')
mock_exists.assert_called_with('/etc/init.d/haproxy-123')
mock_exists.assert_any_call('/var/lib/octavia/123/123.pid')
# service is stopped + init script
mock_exists.side_effect = [True, False, True]
rv = self.app.delete('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(200, rv.status_code)
self.assertEqual({u'message': u'OK'},
json.loads(rv.data.decode('utf-8')))
mock_remove.assert_called_once_with('/etc/init.d/haproxy-123')
# service is running + init script
mock_exists.side_effect = [True, True, True, True]
mock_pid.return_value = '456'
rv = self.app.delete('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(200, rv.status_code)
self.assertEqual({u'message': u'OK'},
json.loads(rv.data.decode('utf-8')))
mock_pid.assert_called_once_with('123')
mock_check_output.assert_called_once_with(
['/usr/sbin/service', 'haproxy-123', 'stop'], stderr=-2)
# service is running + stopping fails
mock_exists.side_effect = [True, True, True]
mock_check_output.side_effect = subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)
rv = self.app.delete('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(500, rv.status_code)
self.assertEqual(
{'details': 'random error', 'message': 'Error stopping haproxy'},
json.loads(rv.data.decode('utf-8')))
# that's the last call before exception
mock_exists.assert_called_with('/proc/456')
@mock.patch('os.path.exists')
def test_get_haproxy(self, mock_exists):
CONTENT = "bibble\nbibble"
mock_exists.side_effect = [False]
rv = self.app.get('/' + api_server.VERSION + '/listeners/123/haproxy')
self.assertEqual(404, rv.status_code)
mock_exists.side_effect = [True]
m = mock.mock_open(read_data=CONTENT)
with mock.patch.object(builtins, 'open', m):
rv = self.app.get('/' + api_server.VERSION +
'/listeners/123/haproxy')
self.assertEqual(200, rv.status_code)
self.assertEqual(six.b(CONTENT), rv.data)
self.assertEqual('text/plain; charset=utf-8',
rv.headers['Content-Type'])
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
+ 'get_listeners')
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
+ '_check_listener_status')
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
+ '_parse_haproxy_file')
def test_get_all_listeners(self, mock_parse, mock_status, mock_listener):
# no listeners
mock_listener.side_effect = [[]]
rv = self.app.get('/' + api_server.VERSION + '/listeners')
self.assertEqual(200, rv.status_code)
self.assertFalse(json.loads(rv.data.decode('utf-8')))
# one listener ACTIVE
mock_listener.side_effect = [['123']]
mock_parse.side_effect = [{'mode': 'test'}]
mock_status.side_effect = [consts.ACTIVE]
rv = self.app.get('/' + api_server.VERSION + '/listeners')
self.assertEqual(200, rv.status_code)
self.assertEqual(
[{'status': consts.ACTIVE, 'type': 'test', 'uuid': '123'}],
json.loads(rv.data.decode('utf-8')))
# two listener one ACTIVE, one ERROR
mock_listener.side_effect = [['123', '456']]
mock_parse.side_effect = [{'mode': 'test'}, {'mode': 'http'}]
mock_status.side_effect = [consts.ACTIVE, consts.ERROR]
rv = self.app.get('/' + api_server.VERSION + '/listeners')
self.assertEqual(200, rv.status_code)
self.assertEqual(
[{'status': consts.ACTIVE, 'type': 'test', 'uuid': '123'},
{'status': consts.ERROR, 'type': 'http', 'uuid': '456'}],
json.loads(rv.data.decode('utf-8')))
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
+ '_check_listener_status')
@mock.patch('octavia.amphorae.backends.agent.api_server.listener.'
+ '_parse_haproxy_file')
@mock.patch('octavia.amphorae.backends.utils.haproxy_query.HAProxyQuery')
@mock.patch('os.path.exists')
def test_get_listener(self, mock_exists, mock_query, mock_parse,
mock_status):
# Listener not found
mock_exists.side_effect = [False]
rv = self.app.get('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(404, rv.status_code)
self.assertEqual(
{'message': 'Listener Not Found',
'details': 'No listener with UUID: 123'},
json.loads(rv.data.decode('utf-8')))
# Listener not ACTIVE
mock_parse.side_effect = [dict(mode='test')]
mock_status.side_effect = [consts.ERROR]
mock_exists.side_effect = [True]
rv = self.app.get('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(200, rv.status_code)
self.assertEqual(dict(
status=consts.ERROR,
type='test',
uuid='123'), json.loads(rv.data.decode('utf-8')))
# Listener ACTIVE
mock_parse.side_effect = [dict(mode='test', stats_socket='blah')]
mock_status.side_effect = [consts.ACTIVE]
mock_exists.side_effect = [True]
mock_pool = mock.Mock()
mock_query.side_effect = [mock_pool]
mock_pool.get_pool_status.side_effect = [
{'tcp-servers': {
'status': 'DOWN',
'uuid': 'tcp-servers',
'members': [
{'id-34833': 'DOWN'},
{'id-34836': 'DOWN'}]}}]
rv = self.app.get('/' + api_server.VERSION + '/listeners/123')
self.assertEqual(200, rv.status_code)
self.assertEqual(dict(
status=consts.ACTIVE,
type='test',
uuid='123',
pools=[dict(
status=consts.DOWN,
uuid='tcp-servers',
members=[
{u'id-34833': u'DOWN'},
{u'id-34836': u'DOWN'}])]),
json.loads(rv.data.decode('utf-8')))
@mock.patch('os.path.exists')
@mock.patch('os.remove')
def test_delete_cert(self, mock_remove, mock_exists):
mock_exists.side_effect = [False]
rv = self.app.delete('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem')
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(
details='No certificate with filename: test.pem',
message='Certificate Not Found'),
json.loads(rv.data.decode('utf-8')))
mock_exists.assert_called_once_with(
'/var/lib/octavia/certs/123/test.pem')
# wrong file name
mock_exists.side_effect = [True]
rv = self.app.put('/' + api_server.VERSION +
'/listeners/123/certificates/test.bla',
data='TestTest')
self.assertEqual(400, rv.status_code)
mock_exists.side_effect = [True]
rv = self.app.delete('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem')
self.assertEqual(200, rv.status_code)
self.assertEqual(OK, json.loads(rv.data.decode('utf-8')))
mock_remove.assert_called_once_with(
'/var/lib/octavia/certs/123/test.pem')
@mock.patch('os.path.exists')
def test_get_certificate_md5(self, mock_exists):
CONTENT = "TestTest"
mock_exists.side_effect = [False]
rv = self.app.get('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem')
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(
details='No certificate with filename: test.pem',
message='Certificate Not Found'),
json.loads(rv.data.decode('utf-8')))
mock_exists.assert_called_with('/var/lib/octavia/certs/123/test.pem')
# wrong file name
mock_exists.side_effect = [True]
rv = self.app.put('/' + api_server.VERSION +
'/listeners/123/certificates/test.bla',
data='TestTest')
self.assertEqual(400, rv.status_code)
m = mock.mock_open(read_data=CONTENT)
mock_exists.return_value = True
mock_exists.side_effect = None
with mock.patch.object(builtins, 'open', m):
rv = self.app.get('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem')
self.assertEqual(200, rv.status_code)
self.assertEqual(dict(md5sum=hashlib.md5(six.b(CONTENT)).hexdigest()),
json.loads(rv.data.decode('utf-8')))
@mock.patch('os.path.exists')
@mock.patch('os.fchmod')
@mock.patch('os.makedirs')
def test_upload_certificate_md5(self, mock_makedir, mock_chmod,
mock_exists):
# wrong file name
mock_exists.side_effect = [True]
rv = self.app.put('/' + api_server.VERSION +
'/listeners/123/certificates/test.bla',
data='TestTest')
self.assertEqual(400, rv.status_code)
mock_exists.side_effect = [True, True, True]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem',
data='TestTest')
self.assertEqual(200, rv.status_code)
self.assertEqual(OK, json.loads(rv.data.decode('utf-8')))
handle = m()
handle.write.assert_called_once_with(six.b('TestTest'))
mock_chmod.assert_called_once_with(handle.fileno(), 0o600)
mock_exists.side_effect = [True, False]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/listeners/123/certificates/test.pem',
data='TestTest')
self.assertEqual(200, rv.status_code)
self.assertEqual(OK, json.loads(rv.data.decode('utf-8')))
handle = m()
mock_makedir.called_once_with('/var/lib/octavia/123')
@mock.patch('os.fchmod')
def test_upload_server_certificate(self, mock_chmod):
certificate_update.BUFFER = 5 # test the while loop
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION +
'/certificate',
data='TestTest')
self.assertEqual(202, rv.status_code)
self.assertEqual(OK, json.loads(rv.data.decode('utf-8')))
handle = m()
handle.write.assert_any_call(six.b('TestT'))
handle.write.assert_any_call(six.b('est'))
mock_chmod.assert_called_once_with(handle.fileno(), 0o600)
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('subprocess.check_output')
def test_plug_network(self, mock_check_output, mock_ifaddress,
mock_interfaces):
port_info = {'mac_address': '123'}
# No interface at all
mock_interfaces.side_effect = [[]]
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# No interface down
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_INET]]
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
mock_ifaddress.assert_called_once_with('blah')
# One Interface down, Happy Path
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(202, rv.status_code)
m.assert_called_once_with(
'/etc/network/interfaces.d/blah.cfg', 'w')
handle = m()
handle.write.assert_called_once_with(
'\n# Generated by Octavia agent\n'
'auto blah blah:0\n'
'iface blah inet dhcp')
mock_check_output.assert_called_with(
['ifup', 'blah'], stderr=-2)
# same as above but ifup fails
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
mock_check_output.side_effect = [subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR), subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(500, rv.status_code)
self.assertEqual(
{'details': RANDOM_ERROR,
'message': 'Error plugging network'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('subprocess.check_output')
@mock.patch('pyroute2.IPRoute')
def test_plug_VIP(self, mock_pyroute2, mock_check_output, mock_ifaddress,
mock_interfaces):
subnet_info = {'subnet_cidr': '10.0.0.0/24',
'gateway': '10.0.0.1',
'mac_address': '123'}
# malformated ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info),
content_type='application/json')
self.assertEqual(400, rv.status_code)
# No subnet info
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error')
self.assertEqual(400, rv.status_code)
# No interface at all
mock_interfaces.side_effect = [[]]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Two interfaces down
mock_interfaces.side_effect = [['blah', 'blah2']]
mock_ifaddress.side_effect = [['blabla'], ['blabla']]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(202, rv.status_code)
m.assert_called_once_with(
'/etc/network/interfaces.d/blah.cfg', 'w')
handle = m()
handle.write.assert_called_once_with(
'\n# Generated by Octavia agent\n'
'auto blah blah:0\n'
'iface blah inet dhcp\n'
'iface blah:0 inet static\n'
'address 203.0.113.2\n'
'broadcast 203.0.113.255\n'
'netmask 255.255.255.0')
mock_check_output.assert_called_with(
['ifup', 'blah:0'], stderr=-2)
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
mock_check_output.side_effect = [
'unplug1',
subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR), subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)]
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(500, rv.status_code)
self.assertEqual(
{'details': RANDOM_ERROR,
'message': 'Error plugging VIP'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.ifaddresses')
@mock.patch('netifaces.interfaces')
def test_get_interface(self, mock_interfaces, mock_ifaddresses):
interface_res = {'interface': 'eth0'}
mock_interfaces.return_value = ['lo', 'eth0']
mock_ifaddresses.return_value = {
17: [{'addr': '00:00:00:00:00:00'}],
2: [{'addr': '203.0.113.2'}],
10: [{'addr': '::1'}]}
rv = self.app.get('/' + api_server.VERSION + '/interface/203.0.113.2',
data=json.dumps(interface_res),
content_type='application/json')
self.assertEqual(200, rv.status_code)
rv = self.app.get('/' + api_server.VERSION + '/interface/::1',
data=json.dumps(interface_res),
content_type='application/json')
self.assertEqual(200, rv.status_code)
rv = self.app.get('/' + api_server.VERSION + '/interface/10.0.0.1',
data=json.dumps(interface_res),
content_type='application/json')
self.assertEqual(404, rv.status_code)
rv = self.app.get('/' + api_server.VERSION +
'/interface/00:00:00:00:00:00',
data=json.dumps(interface_res),
content_type='application/json')
self.assertEqual(500, rv.status_code)
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@mock.patch('os.rename')
@mock.patch('subprocess.check_output')
@mock.patch('os.remove')
def test_upload_keepalived_config(self, mock_remove, mock_subprocess,
mock_rename, mock_makedirs, mock_exists):
mock_exists.return_value = True
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
data='test')
self.assertEqual(200, rv.status_code)
mock_exists.return_value = False
m = mock.mock_open()
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
data='test')
self.assertEqual(200, rv.status_code)
mock_subprocess.side_effect = subprocess.CalledProcessError(1,
'blah!')
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
data='test')
self.assertEqual(500, rv.status_code)
mock_subprocess.side_effect = [True,
subprocess.CalledProcessError(1,
'blah!')]
with mock.patch.object(builtins, 'open', m):
rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload',
data='test')
self.assertEqual(500, rv.status_code)
@mock.patch('subprocess.check_output')
def test_manage_service_vrrp(self, mock_check_output):
rv = self.app.put('/' + api_server.VERSION + '/vrrp/start')
self.assertEqual(202, rv.status_code)
rv = self.app.put('/' + api_server.VERSION + '/vrrp/restart')
self.assertEqual(400, rv.status_code)
mock_check_output.side_effect = subprocess.CalledProcessError(1,
'blah!')
rv = self.app.put('/' + api_server.VERSION + '/vrrp/start')
self.assertEqual(500, rv.status_code)

View File

@ -36,6 +36,7 @@ class AgentJinjaTestCase(base.TestCase):
agent_server_network_dir='/etc/network/interfaces.d/') agent_server_network_dir='/etc/network/interfaces.d/')
self.conf.config(group="haproxy_amphora", self.conf.config(group="haproxy_amphora",
base_cert_dir='/var/lib/octavia/certs') base_cert_dir='/var/lib/octavia/certs')
self.conf.config(group="haproxy_amphora", use_upstart='True')
self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia') self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia')
self.conf.config(group="haproxy_amphora", bind_host='0.0.0.0') self.conf.config(group="haproxy_amphora", bind_host='0.0.0.0')
self.conf.config(group="haproxy_amphora", bind_port=9443) self.conf.config(group="haproxy_amphora", bind_port=9443)
@ -62,7 +63,8 @@ class AgentJinjaTestCase(base.TestCase):
'bind_port = 9443\n' 'bind_port = 9443\n'
'haproxy_cmd = /usr/sbin/haproxy\n' 'haproxy_cmd = /usr/sbin/haproxy\n'
'respawn_count = 2\n' 'respawn_count = 2\n'
'respawn_interval = 2\n\n' 'respawn_interval = 2\n'
'use_upstart = True\n\n'
'[health_manager]\n' '[health_manager]\n'
'controller_ip_port_list = 192.0.2.10:5555\n' 'controller_ip_port_list = 192.0.2.10:5555\n'
'heartbeat_interval = 10\n' 'heartbeat_interval = 10\n'
@ -82,6 +84,7 @@ class AgentJinjaTestCase(base.TestCase):
ajc = agent_jinja_cfg.AgentJinjaTemplater() ajc = agent_jinja_cfg.AgentJinjaTemplater()
self.conf.config(group="amphora_agent", self.conf.config(group="amphora_agent",
agent_server_network_file='/etc/network/interfaces') agent_server_network_file='/etc/network/interfaces')
self.conf.config(group="haproxy_amphora", use_upstart='False')
expected_config = ('\n[DEFAULT]\n' expected_config = ('\n[DEFAULT]\n'
'debug = False\n\n' 'debug = False\n\n'
'[haproxy_amphora]\n' '[haproxy_amphora]\n'
@ -91,7 +94,8 @@ class AgentJinjaTestCase(base.TestCase):
'bind_port = 9443\n' 'bind_port = 9443\n'
'haproxy_cmd = /usr/sbin/haproxy\n' 'haproxy_cmd = /usr/sbin/haproxy\n'
'respawn_count = 2\n' 'respawn_count = 2\n'
'respawn_interval = 2\n\n' 'respawn_interval = 2\n'
'use_upstart = False\n\n'
'[health_manager]\n' '[health_manager]\n'
'controller_ip_port_list = 192.0.2.10:5555\n' 'controller_ip_port_list = 192.0.2.10:5555\n'
'heartbeat_interval = 10\n' 'heartbeat_interval = 10\n'