Fix the amphora image support for RH Linux flavors

Not all Linux flavors accept the same type of configuration to manage
NICs. The amphora-agent must be able to distinguish between different
Linux flavors and choose the appropriate type of jinja2 NIC
configuration template for each one, respectively.

Up until now, The amphora-agent had no notion of the operating system
it is running on, therefore it used NIC configuration templates that
only match Debian based Linux flavors (mostly Ubuntu). Making it
unusable for flavors such as RHEL, Fedora and CentOS.

This fix enhances how the amphora-agent is handling NIC hot plugs.
It will use the appropriate jinja2 template by checking the Amphora
distribution name when needed.

Co-Authored-By: Michael Johnson <johnsomor@gmail.com>

Closes-Bug #1548070

Change-Id: Id99948aec64656a0532afc68e146f0610bff1378
This commit is contained in:
Nir Magnezi 2017-01-12 11:32:50 +02:00
parent 67866ea193
commit c00488143d
38 changed files with 2620 additions and 828 deletions

View File

@ -333,21 +333,21 @@ if [ "$AMP_BASEOS" = "ubuntu" ]; then
export UBUNTU_MIRROR="$BASE_OS_MIRROR"
fi
elif [ "$AMP_BASEOS" = "fedora" ]; then
AMP_element_sequence=${AMP_element_sequence:-"base vm fedora"}
AMP_element_sequence=${AMP_element_sequence:-"base vm fedora selinux-permissive"}
AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
if [ "$BASE_OS_MIRROR" ]; then
AMP_element_sequence="$AMP_element_sequence fedora-mirror"
export FEDORA_MIRROR="$BASE_OS_MIRROR"
fi
elif [ "$AMP_BASEOS" = "centos" ]; then
AMP_element_sequence=${AMP_element_sequence:-"base vm centos7 epel"}
AMP_element_sequence=${AMP_element_sequence:-"base vm centos7 epel selinux-permissive"}
AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
if [ "$BASE_OS_MIRROR" ]; then
AMP_element_sequence="$AMP_element_sequence centos-mirror"
export CENTOS_MIRROR="$BASE_OS_MIRROR"
fi
elif [ "$AMP_BASEOS" = "rhel" ]; then
AMP_element_sequence=${AMP_element_sequence:-"base vm rhel7"}
AMP_element_sequence=${AMP_element_sequence:-"base vm rhel7 selinux-permissive"}
AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
fi

View File

@ -8,6 +8,9 @@ set -o pipefail
pip install -U -c /opt/upper-constraints.txt /opt/amphora-agent
# Accommodate centos default install location
ln -s /bin/amphora-agent /usr/local/bin/amphora-agent || true
mkdir /etc/octavia
# we assume certs, etc will come in through the config drive
mkdir /etc/octavia/certs

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -eux
set -o pipefail
# Allow haproxy to proxy any port if SELinux is in enforcing mode
# https://bugs.launchpad.net/tripleo/+bug/1339938
if [[ -x /usr/sbin/semanage ]]; then
setsebool -P haproxy_connect_any 1
fi

View File

@ -0,0 +1,12 @@
#!/bin/bash
set -eu
set -o pipefail
if [[ -d /etc/rsyslog.d ]] && [[ ! -e /etc/rsyslog.d/49-haproxy.conf ]]; then
cat >> /etc/rsyslog.d/49-haproxy.conf <<EOF
# Send HAProxy messages to a dedicated logfile
if \$programname startswith 'haproxy' then /var/log/haproxy.log
&~
EOF
fi

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -eux
set -o pipefail
# Allow haproxy to proxy any port if SELinux is in enforcing mode
# https://bugs.launchpad.net/tripleo/+bug/1339938
if [[ -x /usr/sbin/semanage ]]; then
setsebool -P haproxy_connect_any 1
fi

View File

View File

@ -0,0 +1,12 @@
#!/bin/bash
set -eu
set -o pipefail
if [[ -d /etc/rsyslog.d ]] && [[ ! -e /etc/rsyslog.d/49-haproxy.conf ]]; then
cat >> /etc/rsyslog.d/49-haproxy.conf <<EOF
# Send HAProxy messages to a dedicated logfile
if \$programname startswith 'haproxy' then /var/log/haproxy.log
&~
EOF
fi

View File

@ -1,6 +0,0 @@
#!/bin/bash
set -eu
set -o pipefail
chkconfig keepalived on

View File

@ -3,6 +3,8 @@ it with data from DHCP. This means that DNS resolution will not work from the
amphora. This is OK because all outbound connections from the amphora will
be based using raw IP addresses.
In addition we remove dns from the nsswitch.conf hosts setting.
This has the real benefit of speeding up host boot and configutation times.
This is especially helpful when running tempest tests in a devstack environment
where DNS resolution from the amphora usually doesn't work anyway: This means

View File

@ -14,3 +14,7 @@ else
make_resolv_conf() { : ; }" > /etc/dhclient-enter-hooks
chmod +x /etc/dhclient-enter-hooks
fi
if [ -e /etc/nsswitch.conf ]; then
sed -i -e "/hosts:/ s/dns//g" /etc/nsswitch.conf
fi

View File

@ -224,7 +224,13 @@
[amphora_agent]
# agent_server_ca = /etc/octavia/certs/client_ca.pem
# agent_server_cert = /etc/octavia/certs/server.pem
# agent_server_network_dir = /etc/netns/amphora-haproxy/network/interfaces.d/
# Defaults for agent_server_network_dir when not specified here are:
# Ubuntu: /etc/netns/amphora-haproxy/network/interfaces.d/
# Centos/fedora/rhel: /etc/netns/amphora-haproxy/sysconfig/network-scripts/
#
# agent_server_network_dir =
# agent_server_network_file =
# agent_request_read_timeout = 120

View File

@ -31,25 +31,30 @@ from octavia.common import constants as consts
LOG = logging.getLogger(__name__)
def compile_amphora_info():
class AmphoraInfo(object):
def __init__(self, osutils):
self._osutils = osutils
def compile_amphora_info(self):
return flask.jsonify(
{'hostname': socket.gethostname(),
'haproxy_version': _get_version_of_installed_package('haproxy'),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION})
def compile_amphora_details():
def compile_amphora_details(self):
listener_list = util.get_listeners()
meminfo = _get_meminfo()
cpu = _cpu()
meminfo = self._get_meminfo()
cpu = self._cpu()
st = os.statvfs('/')
return flask.jsonify(
{'hostname': socket.gethostname(),
'haproxy_version': _get_version_of_installed_package('haproxy'),
'haproxy_version':
self._get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION,
'networks': _get_networks(),
'networks': self._get_networks(),
'active': True,
'haproxy_count': _count_haproxy_processes(listener_list),
'haproxy_count': self._count_haproxy_processes(listener_list),
'cpu': {
'total': cpu['total'],
'user': cpu['user'],
@ -66,22 +71,20 @@ def compile_amphora_details():
'disk': {
'used': (st.f_blocks - st.f_bfree) * st.f_frsize,
'available': st.f_bavail * st.f_frsize},
'load': [
_load()],
'load': self._load(),
'topology': consts.TOPOLOGY_SINGLE,
'topology_status': consts.TOPOLOGY_STATUS_OK,
'listeners': listener_list,
'packages': {}})
def _get_version_of_installed_package(self, name):
def _get_version_of_installed_package(name):
cmd = "dpkg --status {name}".format(name=name)
cmd = self._osutils.cmd_get_version_of_installed_package(name)
out = subprocess.check_output(cmd.split())
m = re.search(b'Version: .*', out)
return m.group(0)[len('Version: '):]
def _count_haproxy_processes(listener_list):
def _count_haproxy_processes(self, listener_list):
num = 0
for listener_id in listener_list:
if util.is_listener_running(listener_id):
@ -89,11 +92,11 @@ def _count_haproxy_processes(listener_list):
num += 1
return num
def _get_meminfo():
def _get_meminfo(self):
re_parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB')
result = dict()
for line in open('/proc/meminfo'):
with open('/proc/meminfo', 'r') as meminfo:
for line in meminfo:
match = re_parser.match(line)
if not match:
continue # skip lines that don't parse
@ -101,8 +104,7 @@ def _get_meminfo():
result[key] = int(value)
return result
def _cpu():
def _cpu(self):
with open('/proc/stat') as f:
cpu = f.readline()
vals = cpu.split(' ')
@ -117,21 +119,20 @@ def _cpu():
'total': sum([int(i) for i in vals[2:]])
}
def _load():
def _load(self):
with open('/proc/loadavg') as f:
load = f.readline()
vals = load.split(' ')
return vals[:3]
def _get_networks():
def _get_networks(self):
networks = dict()
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns:
for interface in netns.get_links():
interface_name = None
for item in interface['attrs']:
if item[0] == 'IFLA_IFNAME' and not item[1].startswith('eth'):
if (item[0] == 'IFLA_IFNAME'
and not item[1].startswith('eth')):
break
elif item[0] == 'IFLA_IFNAME':
interface_name = item[1]
@ -141,8 +142,7 @@ def _get_networks():
network_rx=item[1]['rx_bytes'])
return networks
def get_interface(ip_addr):
def get_interface(self, ip_addr):
try:
ip_version = ipaddress.ip_address(six.text_type(ip_addr)).version

View File

@ -26,6 +26,7 @@ import jinja2
import six
from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import util
from octavia.amphorae.backends.utils import haproxy_query as query
from octavia.common import constants as consts
@ -74,6 +75,9 @@ class Wrapped(object):
class Listener(object):
def __init__(self):
self._osutils = osutils.BaseOS.get_os_util()
def get_haproxy_config(self, listener_id):
"""Gets the haproxy config
@ -171,7 +175,8 @@ class Listener(object):
respawn_count=util.CONF.haproxy_amphora.respawn_count,
respawn_interval=(util.CONF.haproxy_amphora.
respawn_interval),
amphora_nsname=consts.AMPHORA_NAMESPACE
amphora_nsname=consts.AMPHORA_NAMESPACE,
HasIFUPAll=self._osutils.has_ifup_all()
)
text_file.write(text)

View File

@ -0,0 +1,444 @@
# Copyright 2017 Red Hat, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import platform
import shutil
import stat
import subprocess
import flask
import ipaddress
import jinja2
from oslo_config import cfg
import six
from werkzeug import exceptions
from octavia.common import constants as consts
from octavia.common import exceptions as octavia_exceptions
from octavia.common import utils
from octavia.i18n import _LE
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader(
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
class BaseOS(object):
def __init__(self, os_name):
self.os_name = os_name
@classmethod
def get_os_util(cls):
os_name = platform.linux_distribution(full_distribution_name=False)[0]
for subclass in BaseOS.__subclasses__():
if subclass.is_os_name(os_name):
return subclass(os_name)
raise octavia_exceptions.InvalidAmphoraOperatingSystem(os_name=os_name)
def get_network_interface_file(self, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
interface)
network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, interface)
def create_netns_dir(self, network_dir, netns_network_dir, ignore=None):
# We need to setup the netns network directory so that the ifup
# commands used here and in the startup scripts "sees" the right
# interfaces and scripts.
os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE)
shutil.copytree(
network_dir,
'/etc/netns/{netns}/{net_dir}'.format(
netns=consts.AMPHORA_NAMESPACE,
net_dir=netns_network_dir),
symlinks=True,
ignore=ignore)
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip):
# write interface file
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = template_vip.render(
interface=primary_interface,
vip=vip,
vip_ipv6=ip.version is 6,
prefix=utils.netmask_to_prefix(netmask),
broadcast=broadcast,
netmask=netmask,
gateway=gateway,
mtu=mtu,
vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version is 6,
host_routes=render_host_routes,
)
text_file.write(text)
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path, template_port):
# write interface file
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
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(interface_file_path, flags, mode),
'w') as text_file:
text = self._generate_network_file_text(netns_interface, fixed_ips,
mtu, template_port)
text_file.write(text)
def _generate_network_file_text(self, netns_interface, fixed_ips, mtu,
template_port):
text = ''
if fixed_ips is None:
text = template_port.render(interface=netns_interface)
else:
for index, fixed_ip in enumerate(fixed_ips, -1):
if index == -1:
netns_ip_interface = netns_interface
else:
netns_ip_interface = "{int}:{ip}".format(
int=netns_interface, ip=index)
try:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(
ip_addr if six.text_type == type(
ip_addr) else six.u(ip_addr))
network = ipaddress.ip_network(
cidr if six.text_type == type(
cidr) else six.u(cidr))
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version is 6
else network.netmask.exploded)
host_routes = self.get_host_routes(fixed_ip)
except ValueError:
return flask.make_response(flask.jsonify(dict(
message="Invalid network IP")), 400)
new_text = template_port.render(interface=netns_ip_interface,
ipv6=ip.version is 6,
ip_address=ip.exploded,
broadcast=broadcast,
netmask=netmask,
mtu=mtu,
host_routes=host_routes)
text = '\n'.join([text, new_text])
return text
def get_host_routes(self, fixed_ip):
host_routes = []
for hr in fixed_ip.get('host_routes', []):
network = ipaddress.ip_network(
hr['destination'] if isinstance(
hr['destination'], six.text_type) else
six.u(hr['destination']))
host_routes.append({'network': network, 'gw': hr['nexthop']})
return host_routes
def _bring_if_up(self, interface, what):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
cmd = ("ip netns exec {ns} ifup {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error(_LE('Failed to if up {0} due to '
'error: {1}').format(interface, str(e)))
raise exceptions.HTTPException(
response=flask.make_response(flask.jsonify(dict(
message='Error plugging {0}'.format(what),
details=e.output)), 500))
def _bring_if_down(self, interface):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
cmd = ("ip netns exec {ns} ifdown {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
pass
def bring_interfaces_up(self, ip, primary_interface, secondary_interface):
self._bring_if_down(primary_interface)
self._bring_if_down(secondary_interface)
self._bring_if_up(primary_interface, 'VIP')
self._bring_if_up(secondary_interface, 'VIP')
def has_ifup_all(self):
return True
class Ubuntu(BaseOS):
ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2'
@classmethod
def is_os_name(cls, os_name):
return os_name in ['Ubuntu']
def cmd_get_version_of_installed_package(self, package_name):
return "dpkg --status {name}".format(name=package_name)
def get_network_interface_file(self, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
interface + '.cfg')
network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, interface + '.cfg')
def get_network_path(self):
return '/etc/network'
def get_netns_network_dir(self):
network_dir = self.get_network_path()
return os.path.basename(network_dir)
def create_netns_dir(
self, network_dir=None, netns_network_dir=None, ignore=None):
if not netns_network_dir:
netns_network_dir = self.get_netns_network_dir()
if not network_dir:
network_dir = self.get_network_path()
if not ignore:
ignore = shutil.ignore_patterns('eth0*', 'openssh*')
super(Ubuntu, self).create_netns_dir(
network_dir, netns_network_dir, ignore)
def write_interfaces_file(self):
name = '/etc/netns/{}/network/interfaces'.format(
consts.AMPHORA_NAMESPACE)
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(name, flags, mode), 'w') as int_file:
int_file.write('auto lo\n')
int_file.write('iface lo inet loopback\n')
if not CONF.amphora_agent.agent_server_network_file:
int_file.write('source /etc/netns/{}/network/'
'interfaces.d/*.cfg\n'.format(
consts.AMPHORA_NAMESPACE))
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip=None):
if not template_vip:
template_vip = j2_env.get_template(self.ETH_X_VIP_CONF)
super(Ubuntu, self).write_vip_interface_file(
interface_file_path, primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes,
template_vip)
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path=None,
template_port=None):
if not interface_file_path:
interface_file_path = self.get_network_interface_file(
netns_interface)
if not template_port:
template_port = j2_env.get_template(self.ETH_X_PORT_CONF)
super(Ubuntu, self).write_port_interface_file(
netns_interface, fixed_ips, mtu, interface_file_path,
template_port)
def has_ifup_all(self):
return True
class RH(BaseOS):
ETH_X_PORT_CONF = 'rh_plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'rh_plug_vip_ethX.conf.j2'
ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2'
ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2'
@classmethod
def is_os_name(cls, os_name):
return os_name in ['fedora', 'redhat', 'centos']
def cmd_get_version_of_installed_package(self, package_name):
return "rpm -qi {name}".format(name=package_name)
def get_network_interface_file(self, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
'ifcfg-' + interface)
network_dir = consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, 'ifcfg-' + interface)
def get_alias_network_interface_file(self, interface):
return self.get_network_interface_file(interface + ':0')
def get_static_routes_interface_file(self, interface):
return self.get_network_interface_file('route-' + interface)
def get_network_path(self):
return '/etc/sysconfig/network-scripts'
def get_netns_network_dir(self):
network_full_path = self.get_network_path()
network_basename = os.path.basename(network_full_path)
network_dirname = os.path.dirname(network_full_path)
network_prefixdir = os.path.basename(network_dirname)
return os.path.join(network_prefixdir, network_basename)
def create_netns_dir(
self, network_dir=None, netns_network_dir=None, ignore=None):
if not netns_network_dir:
netns_network_dir = self.get_netns_network_dir()
if not network_dir:
network_dir = self.get_network_path()
if not ignore:
ignore = shutil.ignore_patterns('ifcfg-eth0*', 'ifcfg-lo*')
super(RH, self).create_netns_dir(
network_dir, netns_network_dir, ignore)
# Copy /etc/sysconfig/network file
src = '/etc/sysconfig/network'
dst = '/etc/netns/{netns}/sysconfig'.format(
netns=consts.AMPHORA_NAMESPACE)
shutil.copy2(src, dst)
def write_interfaces_file(self):
# No interfaces file in RH based flavors
return
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip=None):
if not template_vip:
template_vip = j2_env.get_template(self.ETH_X_VIP_CONF)
super(RH, self).write_vip_interface_file(
interface_file_path, primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes,
template_vip)
if ip.version == 4:
# Create an IPv4 alias interface, needed in RH based flavors
alias_interface_file_path = self.get_alias_network_interface_file(
primary_interface)
template_vip_alias = j2_env.get_template(self.ETH_X_ALIAS_VIP_CONF)
super(RH, self).write_vip_interface_file(
alias_interface_file_path, primary_interface, vip, ip,
broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip_alias)
if render_host_routes:
routes_interface_file_path = (
self.get_static_routes_interface_file(primary_interface))
template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
self.write_static_routes_interface_file(
routes_interface_file_path, primary_interface,
render_host_routes, template_routes)
def write_static_routes_interface_file(self, interface_file_path,
interface, host_routes,
template_routes):
# write static routes interface file
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = template_routes.render(
interface=interface,
host_routes=host_routes,
)
text_file.write(text)
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path=None,
template_port=None):
if not interface_file_path:
interface_file_path = self.get_network_interface_file(
netns_interface)
if not template_port:
template_port = j2_env.get_template(self.ETH_X_PORT_CONF)
super(RH, self).write_port_interface_file(
netns_interface, fixed_ips, mtu, interface_file_path,
template_port)
if fixed_ips:
host_routes = []
for fixed_ip in fixed_ips:
host_routes.extend(self.get_host_routes(fixed_ip))
routes_interface_file_path = (
self.get_static_routes_interface_file(netns_interface))
template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
self.write_static_routes_interface_file(
routes_interface_file_path, netns_interface,
host_routes, template_routes)
def bring_interfaces_up(self, ip, primary_interface, secondary_interface):
if ip.version == 4:
super(RH, self).bring_interfaces_up(
ip, primary_interface, secondary_interface)
else:
# Secondary interface is not present in IPv6 configuration
self._bring_if_down(primary_interface)
self._bring_if_up(primary_interface, 'VIP')
def has_ifup_all(self):
return False

View File

@ -15,7 +15,6 @@
import logging
import os
import shutil
import socket
import stat
import subprocess
@ -29,9 +28,8 @@ import pyroute2
import six
from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants as consts
from octavia.i18n import _LE, _LI
from octavia.i18n import _LI
CONF = cfg.CONF
@ -48,6 +46,8 @@ template_vip = j2_env.get_template(ETH_X_VIP_CONF)
class Plug(object):
def __init__(self, osutils):
self._osutils = osutils
def plug_vip(self, vip, subnet_cidr, gateway,
mac_address, mtu=None, vrrp_ip=None, host_routes=None):
@ -97,57 +97,24 @@ class Plug(object):
secondary_interface = "{interface}:0".format(
interface=primary_interface)
# We need to setup the netns network directory so that the ifup
# commands used here and in the startup scripts "sees" the right
# interfaces and scripts.
interface_file_path = util.get_network_interface_file(
interface_file_path = self._osutils.get_network_interface_file(
primary_interface)
os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE)
shutil.copytree(
'/etc/network',
'/etc/netns/{}/network'.format(consts.AMPHORA_NAMESPACE),
symlinks=True,
ignore=shutil.ignore_patterns('eth0*', 'openssh*'))
name = '/etc/netns/{}/network/interfaces'.format(
consts.AMPHORA_NAMESPACE)
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(name, flags, mode), 'w') as int_file:
int_file.write('auto lo\n')
int_file.write('iface lo inet loopback\n')
if not CONF.amphora_agent.agent_server_network_file:
int_file.write('source /etc/netns/{}/network/'
'interfaces.d/*.cfg\n'.format(
consts.AMPHORA_NAMESPACE))
# write interface file
self._osutils.create_netns_dir()
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = template_vip.render(
interface=primary_interface,
self._osutils.write_interfaces_file()
self._osutils.write_vip_interface_file(
interface_file_path=interface_file_path,
primary_interface=primary_interface,
vip=vip,
vip_ipv6=ip.version is 6,
ip=ip,
broadcast=broadcast,
netmask=netmask,
gateway=gateway,
mtu=mtu,
vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version is 6,
host_routes=render_host_routes,
)
text_file.write(text)
vrrp_version=vrrp_version,
render_host_routes=render_host_routes)
# Update the list of interfaces to add to the namespace
# This is used in the amphora reboot case to re-establish the namespace
@ -172,60 +139,14 @@ class Plug(object):
IFLA_IFNAME=primary_interface)
# bring interfaces up
self._bring_if_down(primary_interface)
self._bring_if_down(secondary_interface)
self._bring_if_up(primary_interface, 'VIP')
self._bring_if_up(secondary_interface, 'VIP')
self._osutils.bring_interfaces_up(
ip, primary_interface, secondary_interface)
return flask.make_response(flask.jsonify(dict(
message="OK",
details="VIP {vip} plugged on interface {interface}".format(
vip=vip, interface=primary_interface))), 202)
def _generate_network_file_text(self, netns_interface, fixed_ips, mtu):
text = ''
if fixed_ips is None:
text = template_port.render(interface=netns_interface)
else:
for index, fixed_ip in enumerate(fixed_ips, -1):
if index == -1:
netns_ip_interface = netns_interface
else:
netns_ip_interface = "{int}:{ip}".format(
int=netns_interface, ip=index)
try:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(
ip_addr if six.text_type == type(
ip_addr) else six.u(ip_addr))
network = ipaddress.ip_network(
cidr if six.text_type == type(
cidr) else six.u(cidr))
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version is 6
else network.netmask.exploded)
host_routes = []
for hr in fixed_ip.get('host_routes', []):
network = ipaddress.ip_network(
hr['destination'] if isinstance(
hr['destination'], six.text_type) else
six.u(hr['destination']))
host_routes.append({'network': network,
'gw': hr['nexthop']})
except ValueError:
return flask.make_response(flask.jsonify(dict(
message="Invalid network IP")), 400)
new_text = template_port.render(interface=netns_ip_interface,
ipv6=ip.version is 6,
ip_address=ip.exploded,
broadcast=broadcast,
netmask=netmask,
mtu=mtu,
host_routes=host_routes)
text = '\n'.join([text, new_text])
return text
def _check_ip_addresses(self, fixed_ips):
if fixed_ips:
for ip in fixed_ips:
@ -267,27 +188,13 @@ class Plug(object):
'namespace {2}').format(default_netns_interface,
netns_interface,
consts.AMPHORA_NAMESPACE))
interface_file_path = util.get_network_interface_file(netns_interface)
# write interface file
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
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(interface_file_path, flags, mode),
'w') as text_file:
text = self._generate_network_file_text(netns_interface,
fixed_ips,
mtu)
text_file.write(text)
interface_file_path = self._osutils.get_network_interface_file(
netns_interface)
self._osutils.write_port_interface_file(
netns_interface=netns_interface,
fixed_ips=fixed_ips,
mtu=mtu,
interface_file_path=interface_file_path)
# Update the list of interfaces to add to the namespace
self._update_plugged_interfaces_file(netns_interface, mac_address)
@ -299,8 +206,8 @@ class Plug(object):
net_ns_fd=consts.AMPHORA_NAMESPACE,
IFLA_IFNAME=netns_interface)
self._bring_if_down(netns_interface)
self._bring_if_up(netns_interface, 'network')
self._osutils._bring_if_down(netns_interface)
self._osutils._bring_if_up(netns_interface, 'network')
return flask.make_response(flask.jsonify(dict(
message="OK",
@ -318,31 +225,6 @@ class Plug(object):
response=flask.make_response(flask.jsonify(dict(
details="No suitable network interface found")), 404))
def _bring_if_up(self, interface, what):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
cmd = ("ip netns exec {ns} ifup {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error(_LE('Failed to if up {0} due to '
'error: {1}').format(interface, str(e)))
raise exceptions.HTTPException(
response=flask.make_response(flask.jsonify(dict(
message='Error plugging {0}'.format(what),
details=e.output)), 500))
def _bring_if_down(self, interface):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
cmd = ("ip netns exec {ns} ifdown {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
pass
def _update_plugged_interfaces_file(self, interface, mac_address):
# write interfaces to plugged_interfaces file and prevent duplicates
plug_inf_file = consts.PLUGGED_INTERFACES

View File

@ -22,6 +22,7 @@ from octavia.amphorae.backends.agent.api_server import amphora_info
from octavia.amphorae.backends.agent.api_server import certificate_update
from octavia.amphorae.backends.agent.api_server import keepalived
from octavia.amphorae.backends.agent.api_server import listener
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import plug
PATH_PREFIX = '/' + api_server.VERSION
@ -43,9 +44,11 @@ def register_app_error_handler(app):
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._plug = plug.Plug()
self._plug = plug.Plug(self._osutils)
self._amphora_info = amphora_info.AmphoraInfo(self._osutils)
register_app_error_handler(self.app)
@ -119,10 +122,10 @@ class Server(object):
return self._listener.delete_listener(listener_id)
def get_details(self):
return amphora_info.compile_amphora_details()
return self._amphora_info.compile_amphora_details()
def get_info(self):
return amphora_info.compile_amphora_info()
return self._amphora_info.compile_amphora_info()
def get_all_listeners_status(self):
return self._listener.get_all_listeners_status()
@ -179,4 +182,4 @@ class Server(object):
return self._keepalived.manager_keepalived_service(action)
def get_interface(self, ip_addr):
return amphora_info.get_interface(ip_addr)
return self._amphora_info.get_interface(ip_addr)

View File

@ -16,7 +16,6 @@
#}
# Generated by Octavia agent
auto {{ interface }} {{ interface }}:0
{%- if vrrp_ip %}
iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static
address {{ vrrp_ip }}
@ -31,6 +30,7 @@ up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endfor %}
{%- else %}
iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %}

View File

@ -0,0 +1,47 @@
{#
# Copyright 2017 Red Hat, Inc. All rights reserved.
#
# 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
NM_CONTROLLED="no"
DEVICE="{{ interface }}"
ONBOOT="yes"
TYPE="Ethernet"
USERCTL="yes"
{%- if ipv6 %}
IPV6INIT="yes"
{%- if mtu %}
IPV6_MTU="{{ mtu }}"
{%- endif %}
{%- if ip_address %}
IPV6_AUTOCONF="no"
IPV6ADDR="{{ ip_address }}"
{%- else %}
IPV6_AUTOCONF="yes"
{%- endif %}
{%- else %}
IPV6INIT="no"
{%- if mtu %}
MTU="{{ mtu }}"
{%- endif %}
{%- if ip_address %}
BOOTPROTO="static"
IPADDR="{{ ip_address }}"
NETMASK="{{ netmask }}"
{%- else %}
BOOTPROTO="dhcp"
PERSISTENT_DHCLIENT="1"
{%- endif %}
{%- endif %}

View File

@ -0,0 +1,54 @@
{#
# Copyright 2017 Red Hat, Inc. All rights reserved.
#
# 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
NM_CONTROLLED="no"
DEVICE="{{ interface }}"
ONBOOT="yes"
TYPE="Ethernet"
USERCTL="yes"
{%- if vrrp_ip %}
{%- if vrrp_ipv6 %}
IPV6INIT="yes"
IPV6_DEFROUTE="yes"
IPV6_AUTOCONF="no"
IPV6ADDR="{{ vrrp_ip }}/{{ prefix }}"
IPV6_DEFAULTGW="{{ gateway }}"
{%- if mtu %}
IPV6_MTU="{{ mtu }}"
{%- endif %}
{%- else %} {# not vrrp_ipv6 #}
BOOTPROTO="static"
IPADDR="{{ vrrp_ip }}"
NETMASK="{{ netmask }}"
GATEWAY="{{ gateway }}"
MTU="{{ mtu }}"
{%- endif %} {# end if vrrp_ipv6 #}
{%- else %} {# not vrrp_ip #}
{%- if vip_ipv6 %}
IPV6INIT="yes"
IPV6_DEFROUTE="yes"
IPV6_AUTOCONF="yes"
{%- else %}
BOOTPROTO="dhcp"
PERSISTENT_DHCLIENT="1"
{%- endif %} {# end if vip_ipv6 #}
{%- endif %} {# end if vrrp_ip #}
{%- if vip_ipv6 %}
IPV6ADDR_SECONDARIES="{{ vip }}/{{ prefix }}"
{%- endif %}

View File

@ -0,0 +1,29 @@
{#
# Copyright 2017 Red Hat, Inc. All rights reserved.
#
# 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
NM_CONTROLLED="no"
DEVICE="{{ interface }}:0"
NAME="{{ interface }}:0"
ONBOOT="yes"
ARPCHECK="no"
IPV6INIT="no"
{%- if mtu %}
MTU="{{ mtu }}"
{%- endif %}
BOOTPROTO="static"
IPADDR="{{ vip }}"
NETMASK="{{ netmask }}"

View File

@ -0,0 +1,20 @@
{#
# Copyright 2017 Red Hat, Inc. All rights reserved.
#
# 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
{%- for hr in host_routes %}
{{ hr.network }} via {{ hr.gw }} dev {{ interface }}
{%- endfor %}

View File

@ -15,9 +15,13 @@ ExecStartPre=-/sbin/ip netns exec {{ amphora_nsname }} sysctl --system
# We need the plugged_interfaces file sorted to join the host interfaces
ExecStartPre=-/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
ExecStartPre=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} {print $17 " " $2}\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\''
ExecStartPre=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\''
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ExecStartPre=-/sbin/ip netns exec {{ amphora_nsname }} ifup -a
{%- else %}
ExecStartPre=-/bin/awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces
{%- endif %}
#
ExecStart=/sbin/ip netns exec {{ amphora_nsname }} /usr/sbin/haproxy-systemd-wrapper -f {{ haproxy_cfg }} -p {{ haproxy_pid }} -L {{ peer_name }} $EXTRAOPTS
ExecReload=/usr/sbin/haproxy -c -f {{ haproxy_cfg }}

View File

@ -57,9 +57,13 @@ haproxy_start()
# We need the plugged_interfaces file sorted to join the host interfaces
sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted || true
# Assign the interfaces into the namespace with the appropriate name
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $2}' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true
{%- else %}
awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces || true
{%- endif %}
start-stop-daemon --start --pidfile "$PIDFILE" \
--exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \

View File

@ -39,9 +39,14 @@ pre-start script
# interfaces
sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted || true
# Assign the interfaces into the namespace with the appropriate name
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $2}' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true
{%- else %}
awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces || true
{%- endif %}
end script
script

View File

@ -125,13 +125,6 @@ def is_listener_running(listener_id):
os.path.join('/proc', get_haproxy_pid(listener_id)))
def get_network_interface_file(interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
interface + '.cfg')
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

@ -34,7 +34,9 @@ heartbeat_key = {{ heartbeat_key }}
[amphora_agent]
agent_server_ca = {{ agent_server_ca }}
agent_server_cert = {{ agent_server_cert }}
{% if agent_server_network_dir -%}
agent_server_network_dir = {{ agent_server_network_dir }}
{% endif -%}
{% if agent_server_network_file -%}
agent_server_network_file = {{ agent_server_network_file }}
{% endif -%}

View File

@ -69,8 +69,6 @@ amphora_agent_opts = [
help=_("The server certificate for the agent.py server "
"to use")),
cfg.StrOpt('agent_server_network_dir',
default='/etc/netns/{}/network/interfaces.d/'.format(
constants.AMPHORA_NAMESPACE),
help=_("The directory where new network interfaces "
"are located")),
cfg.StrOpt('agent_server_network_file',

View File

@ -366,3 +366,8 @@ KEEPALIVED_UPSTART = 'octavia-keepalived.conf'
# Authentication
KEYSTONE = 'keystone'
NOAUTH = 'noauth'
UBUNTU_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/network/interfaces.d/'
RH_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network-scripts/'
UBUNTU = 'ubuntu'
CENTOS = 'centos'

View File

@ -237,6 +237,10 @@ class ServerGroupObjectDeleteException(OctaviaException):
message = _('Failed to delete server group object.')
class InvalidAmphoraOperatingSystem(OctaviaException):
message = _('Invalid amphora operating system: %(os_name)s')
class QuotaException(APIException):
msg = _('Quota has been met.')
code = 403

View File

@ -74,6 +74,10 @@ def ip_port_str(ip_address, port):
return "[{ip}]:{port}".format(ip=ip, port=port)
def netmask_to_prefix(netmask):
return netaddr.IPAddress(netmask).netmask_bits()
class exception_logger(object):
"""Wrap a function and log raised exception

View File

@ -28,6 +28,9 @@ class OpenFixture(fixtures.Fixture):
def _setUp(self):
self.mock_open = mock.mock_open(read_data=self.contents)
# work around for https://bugs.python.org/issue21258
self.mock_open.return_value.__iter__ = (
lambda self: iter(self.readline, ''))
self._orig_open = open
def replacement_open(name, *args, **kwargs):

View File

@ -0,0 +1,206 @@
# Copyright 2017 Rackspace US, Inc.
#
# 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 random
import mock
from oslo_utils import uuidutils
from octavia.amphorae.backends.agent import api_server
from octavia.amphorae.backends.agent.api_server import amphora_info
from octavia.tests.common import utils as test_utils
import octavia.tests.unit.base as base
class TestAmphoraInfo(base.TestCase):
API_VERSION = random.randrange(0, 10000)
HAPROXY_VERSION = random.randrange(0, 10000)
def setUp(self):
super(TestAmphoraInfo, self).setUp()
self.osutils_mock = mock.MagicMock()
self.amp_info = amphora_info.AmphoraInfo(self.osutils_mock)
@mock.patch.object(amphora_info, "flask")
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'amphora_info.AmphoraInfo._get_version_of_installed_package',
return_value=HAPROXY_VERSION)
@mock.patch('socket.gethostname', return_value='FAKE_HOST')
def test_compile_amphora_info(self, mock_gethostname, mock_pkg_version,
mock_flask):
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}
self.amp_info.compile_amphora_info()
mock_flask.jsonify.assert_called_once_with(expected_dict)
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):
# Test no listeners passed in
result = self.amp_info._count_haproxy_processes([])
self.assertEqual(0, result)
# Test with a listener specified
mock_is_running.side_effect = [True, False]
result = self.amp_info._count_haproxy_processes(
[uuidutils.generate_uuid(), uuidutils.generate_uuid()])
self.assertEqual(1, result)
def test__get_meminfo(self):
# Test live data from host
result = self.amp_info._get_meminfo()
# check that /proc/meminfo is giving us the fields we use
self.assertIn('MemTotal', result)
self.assertIn('MemFree', result)
self.assertIn('Buffers', result)
self.assertIn('Cached', result)
self.assertIn('SwapCached', result)
self.assertIn('Shmem', result)
self.assertIn('Slab', result)
# Known data test
meminfo = ('MemTotal: 21692784 kB\n'
'MemFree: 12685624 kB\n'
'MemAvailable: 17384072 kB\n'
'Buffers: 344792 kB\n'
'Cached: 4271856 kB\n'
'SwapCached: 0 kB\n'
'Active: 5808816 kB\n'
'Inactive: 2445236 kB\n'
'Active(anon): 3646184 kB\n'
'Inactive(anon): 8672 kB\n'
'Active(file): 2162632 kB\n'
'Inactive(file): 2436564 kB\n'
'Unevictable: 52664 kB\n'
'Mlocked: 52664 kB\n'
'SwapTotal: 20476924 kB\n'
'SwapFree: 20476924 kB\n'
'Dirty: 92 kB\n'
'Writeback: 0 kB\n'
'AnonPages: 3690088 kB\n'
'Mapped: 108520 kB\n'
'Shmem: 9520 kB\n'
'Slab: 534384 kB\n'
'SReclaimable: 458160 kB\n'
'SUnreclaim: 76224 kB\n'
'KernelStack: 11776 kB\n'
'PageTables: 33088 kB\n'
'NFS_Unstable: 0 kB\n'
'Bounce: 0 kB\n'
'WritebackTmp: 0 kB\n'
'CommitLimit: 31323316 kB\n'
'Committed_AS: 6930732 kB\n'
'VmallocTotal: 34359738367 kB\n'
'VmallocUsed: 0 kB\n'
'VmallocChunk: 0 kB\n'
'HardwareCorrupted: 0 kB\n'
'AnonHugePages: 1400832 kB\n'
'CmaTotal: 0 kB\n'
'CmaFree: 0 kB\n'
'HugePages_Total: 0\n'
'HugePages_Free: 0\n'
'HugePages_Rsvd: 0\n'
'HugePages_Surp: 0\n'
'Hugepagesize: 2048 kB\n'
'DirectMap4k: 130880 kB\n'
'DirectMap2M: 8376320 kB\n'
'DirectMap1G: 14680064 kB\n')
self.useFixture(test_utils.OpenFixture('/proc/meminfo',
contents=meminfo))
expected_result = {'SwapCached': 0, 'DirectMap2M': 8376320,
'CmaTotal': 0, 'Inactive': 2445236,
'KernelStack': 11776, 'SwapTotal': 20476924,
'VmallocUsed': 0, 'Buffers': 344792,
'MemTotal': 21692784, 'Mlocked': 52664,
'Cached': 4271856, 'AnonPages': 3690088,
'Unevictable': 52664, 'SUnreclaim': 76224,
'MemFree': 12685624, 'Writeback': 0,
'NFS_Unstable': 0, 'VmallocTotal': 34359738367,
'MemAvailable': 17384072, 'CmaFree': 0,
'SwapFree': 20476924, 'AnonHugePages': 1400832,
'DirectMap1G': 14680064, 'Hugepagesize': 2048,
'Dirty': 92, 'Bounce': 0, 'PageTables': 33088,
'SReclaimable': 458160, 'Active': 5808816,
'Mapped': 108520, 'Slab': 534384,
'Active(anon)': 3646184, 'VmallocChunk': 0,
'Inactive(file)': 2436564, 'WritebackTmp': 0,
'Shmem': 9520, 'Inactive(anon)': 8672,
'HardwareCorrupted': 0, 'Active(file)': 2162632,
'DirectMap4k': 130880, 'Committed_AS': 6930732,
'CommitLimit': 31323316}
result = self.amp_info._get_meminfo()
self.assertEqual(expected_result, result)
def test__cpu(self):
sample_stat = 'cpu 252551 802 52554 7181757 7411 0 8336 0 0 0'
expected_result = {'user': '252551', 'iowait': '7411', 'nice': '802',
'softirq': '8336', 'idle': '7181757',
'system': '52554', 'total': 7503411, 'irq': '0'}
self.useFixture(test_utils.OpenFixture('/proc/stat',
contents=sample_stat))
result = self.amp_info._cpu()
self.assertEqual(expected_result, result)
def test__load(self):
sample_loadavg = '0.09 0.11 0.10 2/630 15346'
expected_result = ['0.09', '0.11', '0.10']
self.useFixture(test_utils.OpenFixture('/proc/loadavg',
contents=sample_loadavg))
result = self.amp_info._load()
self.assertEqual(expected_result, result)
@mock.patch('pyroute2.NetNS')
def test__get_networks(self, mock_netns):
# The output of get_links is huge, just pulling out the parts we
# care about for this test.
sample_get_links_minimal = [
{'attrs': [('IFLA_IFNAME', 'lo')]},
{'attrs': [('IFLA_IFNAME', 'eth1'),
('IFLA_STATS64', {'tx_bytes': 418, 'rx_bytes': 996})]},
{'attrs': [('IFLA_IFNAME', 'eth2'),
('IFLA_STATS64', {'tx_bytes': 578, 'rx_bytes': 848})]},
{'attrs': [('IFLA_IFNAME', 'eth3')]}]
netns_handle = mock_netns.return_value.__enter__.return_value
netns_handle.get_links.return_value = sample_get_links_minimal
expected_result = {'eth1': {'network_rx': 996, 'network_tx': 418},
'eth2': {'network_rx': 848, 'network_tx': 578}}
result = self.amp_info._get_networks()
self.assertEqual(expected_result, result)

View File

@ -0,0 +1,217 @@
# Copyright 2017 Redhat.
#
# 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 shutil
import mock
from oslo_config import fixture as oslo_fixture
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.common import config
from octavia.common import constants as consts
from octavia.common import exceptions as octavia_exceptions
from octavia.tests.unit import base
class TestOSUtils(base.TestCase):
def setUp(self):
super(TestOSUtils, self).setUp()
self.base_os_util = osutils.BaseOS('unknown')
with mock.patch('platform.linux_distribution',
return_value=['Ubuntu', 'Foo', 'Bar']):
self.ubuntu_os_util = osutils.BaseOS.get_os_util()
with mock.patch('platform.linux_distribution',
return_value=['centos', 'Foo', 'Bar']):
self.rh_os_util = osutils.BaseOS.get_os_util()
def test_get_os_util(self):
with mock.patch('platform.linux_distribution',
return_value=['Ubuntu', 'Foo', 'Bar']):
returned_cls = osutils.BaseOS.get_os_util()
self.assertIsInstance(returned_cls, osutils.Ubuntu)
with mock.patch('platform.linux_distribution',
return_value=['fedora', 'Foo', 'Bar']):
returned_cls = osutils.BaseOS.get_os_util()
self.assertIsInstance(returned_cls, osutils.RH)
with mock.patch('platform.linux_distribution',
return_value=['redhat', 'Foo', 'Bar']):
returned_cls = osutils.BaseOS.get_os_util()
self.assertIsInstance(returned_cls, osutils.RH)
with mock.patch('platform.linux_distribution',
return_value=['centos', 'Foo', 'Bar']):
returned_cls = osutils.BaseOS.get_os_util()
self.assertIsInstance(returned_cls, osutils.RH)
with mock.patch('platform.linux_distribution',
return_value=['FakeOS', 'Foo', 'Bar']):
self.assertRaises(
octavia_exceptions.InvalidAmphoraOperatingSystem,
osutils.BaseOS.get_os_util)
def test_get_network_interface_file(self):
conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF))
fake_agent_server_network_dir = "/path/to/interface"
fake_agent_server_network_file = "/path/to/interfaces_file"
base_fake_nic_path = os.path.join(fake_agent_server_network_dir,
consts.NETNS_PRIMARY_INTERFACE)
base_real_nic_path = os.path.join(
consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
consts.NETNS_PRIMARY_INTERFACE)
rh_interface_name = 'ifcfg-{nic}'.format(
nic=consts.NETNS_PRIMARY_INTERFACE)
rh_fake_nic_path = os.path.join(fake_agent_server_network_dir,
rh_interface_name)
rh_real_nic_path = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_interface_name)
ubuntu_interface_name = '{nic}.cfg'.format(
nic=consts.NETNS_PRIMARY_INTERFACE)
ubuntu_fake_nic_path = os.path.join(fake_agent_server_network_dir,
ubuntu_interface_name)
ubuntu_real_nic_path = os.path.join(
consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
ubuntu_interface_name)
# Check that agent_server_network_file is returned, when provided
conf.config(group="amphora_agent",
agent_server_network_file=fake_agent_server_network_file)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, ubuntu_interface_file)
# Check that agent_server_network_dir is used, when provided
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent",
agent_server_network_dir=fake_agent_server_network_dir)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(base_fake_nic_path, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(rh_fake_nic_path, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(ubuntu_fake_nic_path, ubuntu_interface_file)
# Check When neither agent_server_network_dir or
# agent_server_network_file where provided.
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent", agent_server_network_dir=None)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(base_real_nic_path, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(rh_real_nic_path, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(ubuntu_real_nic_path, ubuntu_interface_file)
def test_cmd_get_version_of_installed_package(self):
package_name = 'foo'
ubuntu_cmd = "dpkg --status {name}".format(name=package_name)
rh_cmd = "rpm -qi {name}".format(name=package_name)
returned_ubuntu_cmd = (
self.ubuntu_os_util.cmd_get_version_of_installed_package(
package_name))
self.assertEqual(ubuntu_cmd, returned_ubuntu_cmd)
returned_rh_cmd = (self.rh_os_util.
cmd_get_version_of_installed_package(package_name))
self.assertEqual(rh_cmd, returned_rh_cmd)
def test_has_ifup_all(self):
self.assertTrue(self.base_os_util.has_ifup_all())
self.assertTrue(self.ubuntu_os_util.has_ifup_all())
self.assertFalse(self.rh_os_util.has_ifup_all())
@mock.patch('shutil.copy2')
@mock.patch('os.makedirs')
@mock.patch('shutil.copytree')
def test_create_netns_dir(self, mock_copytree, mock_makedirs, mock_copy2):
network_dir = 'foo'
netns_network_dir = 'fake_netns_network'
ignore = shutil.ignore_patterns('fake_eth*', 'fake_loopback*')
self.rh_os_util.create_netns_dir(network_dir,
netns_network_dir,
ignore)
mock_copytree.assert_any_call(
network_dir,
os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE,
netns_network_dir),
ignore=ignore,
symlinks=True)
mock_makedirs.assert_any_call(os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE))
mock_copy2.assert_any_call(
'/etc/sysconfig/network',
'/etc/netns/{netns}/sysconfig'.format(
netns=consts.AMPHORA_NAMESPACE))
mock_copytree.reset_mock()
mock_makedirs.reset_mock()
mock_copy2.reset_mock()
self.ubuntu_os_util.create_netns_dir(network_dir,
netns_network_dir,
ignore)
mock_copytree.assert_any_call(
network_dir,
os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE,
netns_network_dir),
ignore=ignore,
symlinks=True)
mock_makedirs.assert_any_call(os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE))
mock_copy2.assert_not_called()

View File

@ -18,6 +18,7 @@ import subprocess
import mock
import netifaces
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import plug
import octavia.tests.unit.base as base
@ -36,7 +37,8 @@ class TestPlug(base.TestCase):
def setUp(self):
super(TestPlug, self).setUp()
self.mock_netifaces = mock.patch.object(plug, "netifaces").start()
self.test_plug = plug.Plug()
self.osutil = osutils.BaseOS.get_os_util()
self.test_plug = plug.Plug(self.osutil)
self.addCleanup(self.mock_netifaces.stop)
# Set up our fake interface
@ -48,10 +50,19 @@ class TestPlug(base.TestCase):
]
}
def test__interface_by_mac_case_insensitive(self):
def test__interface_by_mac_case_insensitive_ubuntu(self):
interface = self.test_plug._interface_by_mac(FAKE_MAC_ADDRESS.upper())
self.assertEqual(FAKE_INTERFACE, interface)
def test__interface_by_mac_case_insensitive_rh(self):
with mock.patch('platform.linux_distribution',
return_value=['centos', 'Foo']):
osutil = osutils.BaseOS.get_os_util()
self.test_plug = plug.Plug(osutil)
interface = self.test_plug._interface_by_mac(
FAKE_MAC_ADDRESS.upper())
self.assertEqual(FAKE_INTERFACE, interface)
@mock.patch('pyroute2.NSPopen')
@mock.patch.object(plug, "flask")
@mock.patch('pyroute2.IPRoute')
@ -146,9 +157,10 @@ class TestPlug(base.TestCase):
class TestPlugNetwork(base.TestCase):
def setUp(self):
super(TestPlugNetwork, self).setUp()
self.test_plug = plug.Plug()
self.osutil = osutils.BaseOS.get_os_util()
self.test_plug = plug.Plug(self.osutil)
def test__generate_network_file_text_static_ip(self):
def test__generate_network_file_text_static_ip_ubuntu(self):
netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2'
BROADCAST = '192.0.2.255'
@ -177,8 +189,9 @@ class TestPlugNetwork(base.TestCase):
'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n')
text = self.test_plug._generate_network_file_text(netns_interface,
fixed_ips, MTU)
template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2')
text = self.test_plug._osutils._generate_network_file_text(
netns_interface, fixed_ips, MTU, template_port)
expected_text = format_text.format(netns_interface=netns_interface,
fixed_ip=FIXED_IP,
broadcast=BROADCAST,

View File

@ -1,47 +0,0 @@
# 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.
import os
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from octavia.amphorae.backends.agent.api_server import util
from octavia.tests.unit import base
class TestUtils(base.TestCase):
def setUp(self):
self.dir = '/etc/network/interfaces.d'
self.file = '/etc/network/interfaces'
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
self.conf.config(group="amphora_agent",
agent_server_network_dir=self.dir)
super(TestUtils, self).setUp()
def test_get_network_interface_file(self):
interface = 'eth0'
self.conf.config(group="amphora_agent",
agent_server_network_file=None)
path = util.get_network_interface_file(interface)
expected_path = os.path.join(self.dir, interface + '.cfg')
self.assertEqual(expected_path, path)
self.conf.config(group="amphora_agent",
agent_server_network_file=self.file)
path = util.get_network_interface_file(interface)
self.assertEqual(self.file, path)

View File

@ -42,3 +42,9 @@ class TestConfig(base.TestCase):
utils.ip_port_str('127.0.0.1', 8080))
self.assertEqual("[::1]:8080",
utils.ip_port_str('::1', 8080))
def test_netmask_to_prefix(self):
self.assertEqual(utils.netmask_to_prefix('255.0.0.0'), 8)
self.assertEqual(utils.netmask_to_prefix('255.255.0.0'), 16)
self.assertEqual(utils.netmask_to_prefix('255.255.255.0'), 24)
self.assertEqual(utils.netmask_to_prefix('255.255.255.128'), 25)

View File

@ -0,0 +1,18 @@
---
prelude: >
Amphora image support for RH Linux flavors.
features:
- The diskimage-create script supports different operating system flavors
such as Ubuntu (the default option), CentOS, Fedora and RHEL. Adaptations
were made to several elements to ensure all images are operational.
- The amphora-agent is now able to distinguish between operating systems and
choose the right course of action to manage files and networking on each
Linux flavor.
issues:
- To use CentOS, Fedora, or RHEL in your amphora image you must set
the user_group option, located in the [haproxy_amphora] section of the
octavia.conf file to "haproxy". This will be made automatic in a
future version.
upgrade:
- agent_server_network_dir is now auto-detected for Ubuntu, CentOS, Fedora
and RHEL if one is not specified in the configuration file.