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" export UBUNTU_MIRROR="$BASE_OS_MIRROR"
fi fi
elif [ "$AMP_BASEOS" = "fedora" ]; then 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" AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
if [ "$BASE_OS_MIRROR" ]; then if [ "$BASE_OS_MIRROR" ]; then
AMP_element_sequence="$AMP_element_sequence fedora-mirror" AMP_element_sequence="$AMP_element_sequence fedora-mirror"
export FEDORA_MIRROR="$BASE_OS_MIRROR" export FEDORA_MIRROR="$BASE_OS_MIRROR"
fi fi
elif [ "$AMP_BASEOS" = "centos" ]; then 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" AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
if [ "$BASE_OS_MIRROR" ]; then if [ "$BASE_OS_MIRROR" ]; then
AMP_element_sequence="$AMP_element_sequence centos-mirror" AMP_element_sequence="$AMP_element_sequence centos-mirror"
export CENTOS_MIRROR="$BASE_OS_MIRROR" export CENTOS_MIRROR="$BASE_OS_MIRROR"
fi fi
elif [ "$AMP_BASEOS" = "rhel" ]; then 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" AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND"
fi fi

View File

@ -8,6 +8,9 @@ set -o pipefail
pip install -U -c /opt/upper-constraints.txt /opt/amphora-agent 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 mkdir /etc/octavia
# we assume certs, etc will come in through the config drive # we assume certs, etc will come in through the config drive
mkdir /etc/octavia/certs 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 amphora. This is OK because all outbound connections from the amphora will
be based using raw IP addresses. 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 has the real benefit of speeding up host boot and configutation times.
This is especially helpful when running tempest tests in a devstack environment 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 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 make_resolv_conf() { : ; }" > /etc/dhclient-enter-hooks
chmod +x /etc/dhclient-enter-hooks chmod +x /etc/dhclient-enter-hooks
fi 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] [amphora_agent]
# agent_server_ca = /etc/octavia/certs/client_ca.pem # agent_server_ca = /etc/octavia/certs/client_ca.pem
# agent_server_cert = /etc/octavia/certs/server.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_server_network_file =
# agent_request_read_timeout = 120 # agent_request_read_timeout = 120

View File

@ -31,177 +31,177 @@ from octavia.common import constants as consts
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def compile_amphora_info(): class AmphoraInfo(object):
return flask.jsonify( def __init__(self, osutils):
{'hostname': socket.gethostname(), self._osutils = osutils
'haproxy_version': _get_version_of_installed_package('haproxy'),
'api_version': api_server.VERSION})
def compile_amphora_info(self):
return flask.jsonify(
{'hostname': socket.gethostname(),
'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() listener_list = util.get_listeners()
meminfo = _get_meminfo() meminfo = self._get_meminfo()
cpu = _cpu() cpu = self._cpu()
st = os.statvfs('/') st = os.statvfs('/')
return flask.jsonify( return flask.jsonify(
{'hostname': socket.gethostname(), {'hostname': socket.gethostname(),
'haproxy_version': _get_version_of_installed_package('haproxy'), 'haproxy_version':
'api_version': api_server.VERSION, self._get_version_of_installed_package('haproxy'),
'networks': _get_networks(), 'api_version': api_server.VERSION,
'active': True, 'networks': self._get_networks(),
'haproxy_count': _count_haproxy_processes(listener_list), 'active': True,
'cpu': { 'haproxy_count': self._count_haproxy_processes(listener_list),
'total': cpu['total'], 'cpu': {
'user': cpu['user'], 'total': cpu['total'],
'system': cpu['system'], 'user': cpu['user'],
'soft_irq': cpu['softirq'], }, 'system': cpu['system'],
'memory': { 'soft_irq': cpu['softirq'], },
'total': meminfo['MemTotal'], 'memory': {
'free': meminfo['MemFree'], 'total': meminfo['MemTotal'],
'buffers': meminfo['Buffers'], 'free': meminfo['MemFree'],
'cached': meminfo['Cached'], 'buffers': meminfo['Buffers'],
'swap_used': meminfo['SwapCached'], 'cached': meminfo['Cached'],
'shared': meminfo['Shmem'], 'swap_used': meminfo['SwapCached'],
'slab': meminfo['Slab'], }, 'shared': meminfo['Shmem'],
'disk': { 'slab': meminfo['Slab'], },
'used': (st.f_blocks - st.f_bfree) * st.f_frsize, 'disk': {
'available': st.f_bavail * st.f_frsize}, 'used': (st.f_blocks - st.f_bfree) * st.f_frsize,
'load': [ 'available': st.f_bavail * st.f_frsize},
_load()], 'load': self._load(),
'topology': consts.TOPOLOGY_SINGLE, 'topology': consts.TOPOLOGY_SINGLE,
'topology_status': consts.TOPOLOGY_STATUS_OK, 'topology_status': consts.TOPOLOGY_STATUS_OK,
'listeners': listener_list, 'listeners': listener_list,
'packages': {}}) 'packages': {}})
def _get_version_of_installed_package(self, name):
def _get_version_of_installed_package(name): cmd = self._osutils.cmd_get_version_of_installed_package(name)
cmd = "dpkg --status {name}".format(name=name) out = subprocess.check_output(cmd.split())
out = subprocess.check_output(cmd.split()) m = re.search(b'Version: .*', out)
m = re.search(b'Version: .*', out) return m.group(0)[len('Version: '):]
return m.group(0)[len('Version: '):]
def _count_haproxy_processes(self, listener_list):
num = 0
for listener_id in listener_list:
if util.is_listener_running(listener_id):
# optional check if it's still running
num += 1
return num
def _count_haproxy_processes(listener_list): def _get_meminfo(self):
num = 0 re_parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB')
for listener_id in listener_list: result = dict()
if util.is_listener_running(listener_id): with open('/proc/meminfo', 'r') as meminfo:
# optional check if it's still running for line in meminfo:
num += 1 match = re_parser.match(line)
return num if not match:
continue # skip lines that don't parse
key, value = match.groups(['key', 'value'])
result[key] = int(value)
return result
def _cpu(self):
with open('/proc/stat') as f:
cpu = f.readline()
vals = cpu.split(' ')
return {
'user': vals[2],
'nice': vals[3],
'system': vals[4],
'idle': vals[5],
'iowait': vals[6],
'irq': vals[7],
'softirq': vals[8],
'total': sum([int(i) for i in vals[2:]])
}
def _get_meminfo(): def _load(self):
re_parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB') with open('/proc/loadavg') as f:
result = dict() load = f.readline()
for line in open('/proc/meminfo'): vals = load.split(' ')
match = re_parser.match(line) return vals[:3]
if not match:
continue # skip lines that don't parse
key, value = match.groups(['key', 'value'])
result[key] = int(value)
return result
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')):
break
elif item[0] == 'IFLA_IFNAME':
interface_name = item[1]
if item[0] == 'IFLA_STATS64':
networks[interface_name] = dict(
network_tx=item[1]['tx_bytes'],
network_rx=item[1]['rx_bytes'])
return networks
def _cpu(): def get_interface(self, ip_addr):
with open('/proc/stat') as f:
cpu = f.readline()
vals = cpu.split(' ')
return {
'user': vals[2],
'nice': vals[3],
'system': vals[4],
'idle': vals[5],
'iowait': vals[6],
'irq': vals[7],
'softirq': vals[8],
'total': sum([int(i) for i in vals[2:]])
}
try:
ip_version = ipaddress.ip_address(six.text_type(ip_addr)).version
except Exception:
return flask.make_response(
flask.jsonify(dict(message="Invalid IP address")), 400)
def _load(): if ip_version == 4:
with open('/proc/loadavg') as f: address_format = netifaces.AF_INET
load = f.readline() elif ip_version == 6:
vals = load.split(' ') address_format = netifaces.AF_INET6
return vals[:3] else:
return flask.make_response(
flask.jsonify(dict(message="Bad IP address version")), 400)
# We need to normalize the address as IPv6 has multiple representations
# fe80:0000:0000:0000:f816:3eff:fef2:2058 == fe80::f816:3eff:fef2:2058
normalized_addr = socket.inet_ntop(address_format,
socket.inet_pton(address_format,
ip_addr))
def _get_networks(): with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns:
networks = dict() for addr in netns.get_addr():
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns: # Save the interface index as IPv6 records don't list a
for interface in netns.get_links(): # textual interface
interface_name = None interface_idx = addr['index']
for item in interface['attrs']: # Save the address family (IPv4/IPv6) for use normalizing
if item[0] == 'IFLA_IFNAME' and not item[1].startswith('eth'): # the IP address for comparison
break interface_af = addr['family']
elif item[0] == 'IFLA_IFNAME': # Search through the attributes of each address record
interface_name = item[1] for attr in addr['attrs']:
if item[0] == 'IFLA_STATS64': # Look for the attribute name/value pair for the address
networks[interface_name] = dict( if attr[0] == 'IFA_ADDRESS':
network_tx=item[1]['tx_bytes'], # Compare the normalized address with the address we
network_rx=item[1]['rx_bytes']) # we are looking for. Since we have matched the name
return networks # above, attr[1] is the address value
if normalized_addr == socket.inet_ntop(
interface_af,
socket.inet_pton(interface_af, attr[1])):
# Lookup the matching interface name by
# getting the interface with the index we found
# in the above address search
lookup_int = netns.get_links(interface_idx)
# Search through the attributes of the matching
# interface record
for int_attr in lookup_int[0]['attrs']:
# Look for the attribute name/value pair
# that includes the interface name
if int_attr[0] == 'IFLA_IFNAME':
# Return the response with the matching
# interface name that is in int_attr[1]
# for the matching interface attribute
# name
return flask.make_response(
flask.jsonify(
dict(message='OK',
interface=int_attr[1])), 200)
def get_interface(ip_addr):
try:
ip_version = ipaddress.ip_address(six.text_type(ip_addr)).version
except Exception:
return flask.make_response( return flask.make_response(
flask.jsonify(dict(message="Invalid IP address")), 400) flask.jsonify(dict(message="Error interface not found "
"for IP address")), 404)
if ip_version == 4:
address_format = netifaces.AF_INET
elif ip_version == 6:
address_format = netifaces.AF_INET6
else:
return flask.make_response(
flask.jsonify(dict(message="Bad IP address version")), 400)
# We need to normalize the address as IPv6 has multiple representations
# fe80:0000:0000:0000:f816:3eff:fef2:2058 == fe80::f816:3eff:fef2:2058
normalized_addr = socket.inet_ntop(address_format,
socket.inet_pton(address_format,
ip_addr))
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns:
for addr in netns.get_addr():
# Save the interface index as IPv6 records don't list a
# textual interface
interface_idx = addr['index']
# Save the address family (IPv4/IPv6) for use normalizing
# the IP address for comparison
interface_af = addr['family']
# Search through the attributes of each address record
for attr in addr['attrs']:
# Look for the attribute name/value pair for the address
if attr[0] == 'IFA_ADDRESS':
# Compare the normalized address with the address we
# we are looking for. Since we have matched the name
# above, attr[1] is the address value
if normalized_addr == socket.inet_ntop(
interface_af,
socket.inet_pton(interface_af, attr[1])):
# Lookup the matching interface name by
# getting the interface with the index we found
# in the above address search
lookup_int = netns.get_links(interface_idx)
# Search through the attributes of the matching
# interface record
for int_attr in lookup_int[0]['attrs']:
# Look for the attribute name/value pair
# that includes the interface name
if int_attr[0] == 'IFLA_IFNAME':
# Return the response with the matching
# interface name that is in int_attr[1]
# for the matching interface attribute
# name
return flask.make_response(
flask.jsonify(
dict(message='OK',
interface=int_attr[1])), 200)
return flask.make_response(
flask.jsonify(dict(message="Error interface not found "
"for IP address")), 404)

View File

@ -26,6 +26,7 @@ import jinja2
import six import six
from werkzeug import exceptions 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.agent.api_server import util
from octavia.amphorae.backends.utils import haproxy_query as query from octavia.amphorae.backends.utils import haproxy_query as query
from octavia.common import constants as consts from octavia.common import constants as consts
@ -74,6 +75,9 @@ class Wrapped(object):
class Listener(object): class Listener(object):
def __init__(self):
self._osutils = osutils.BaseOS.get_os_util()
def get_haproxy_config(self, listener_id): def get_haproxy_config(self, listener_id):
"""Gets the haproxy config """Gets the haproxy config
@ -171,7 +175,8 @@ class Listener(object):
respawn_count=util.CONF.haproxy_amphora.respawn_count, respawn_count=util.CONF.haproxy_amphora.respawn_count,
respawn_interval=(util.CONF.haproxy_amphora. respawn_interval=(util.CONF.haproxy_amphora.
respawn_interval), respawn_interval),
amphora_nsname=consts.AMPHORA_NAMESPACE amphora_nsname=consts.AMPHORA_NAMESPACE,
HasIFUPAll=self._osutils.has_ifup_all()
) )
text_file.write(text) 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 logging
import os import os
import shutil
import socket import socket
import stat import stat
import subprocess import subprocess
@ -29,9 +28,8 @@ import pyroute2
import six import six
from werkzeug import exceptions from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants as consts from octavia.common import constants as consts
from octavia.i18n import _LE, _LI from octavia.i18n import _LI
CONF = cfg.CONF CONF = cfg.CONF
@ -48,6 +46,8 @@ template_vip = j2_env.get_template(ETH_X_VIP_CONF)
class Plug(object): class Plug(object):
def __init__(self, osutils):
self._osutils = osutils
def plug_vip(self, vip, subnet_cidr, gateway, def plug_vip(self, vip, subnet_cidr, gateway,
mac_address, mtu=None, vrrp_ip=None, host_routes=None): mac_address, mtu=None, vrrp_ip=None, host_routes=None):
@ -97,57 +97,24 @@ class Plug(object):
secondary_interface = "{interface}:0".format( secondary_interface = "{interface}:0".format(
interface=primary_interface) interface=primary_interface)
# We need to setup the netns network directory so that the ifup interface_file_path = self._osutils.get_network_interface_file(
# commands used here and in the startup scripts "sees" the right
# interfaces and scripts.
interface_file_path = util.get_network_interface_file(
primary_interface) 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 self._osutils.write_interfaces_file()
self._osutils.write_vip_interface_file(
# If we are using a consolidated interfaces file, just append interface_file_path=interface_file_path,
# otherwise clear the per interface file as we are rewriting it primary_interface=primary_interface,
# TODO(johnsom): We need a way to clean out old interfaces records vip=vip,
if CONF.amphora_agent.agent_server_network_file: ip=ip,
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND broadcast=broadcast,
else: netmask=netmask,
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC gateway=gateway,
mtu=mtu,
with os.fdopen(os.open(interface_file_path, flags, mode), vrrp_ip=vrrp_ip,
'w') as text_file: vrrp_version=vrrp_version,
text = template_vip.render( render_host_routes=render_host_routes)
interface=primary_interface,
vip=vip,
vip_ipv6=ip.version is 6,
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)
# Update the list of interfaces to add to the namespace # Update the list of interfaces to add to the namespace
# This is used in the amphora reboot case to re-establish 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) IFLA_IFNAME=primary_interface)
# bring interfaces up # bring interfaces up
self._bring_if_down(primary_interface) self._osutils.bring_interfaces_up(
self._bring_if_down(secondary_interface) ip, primary_interface, secondary_interface)
self._bring_if_up(primary_interface, 'VIP')
self._bring_if_up(secondary_interface, 'VIP')
return flask.make_response(flask.jsonify(dict( return flask.make_response(flask.jsonify(dict(
message="OK", message="OK",
details="VIP {vip} plugged on interface {interface}".format( details="VIP {vip} plugged on interface {interface}".format(
vip=vip, interface=primary_interface))), 202) 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): def _check_ip_addresses(self, fixed_ips):
if fixed_ips: if fixed_ips:
for ip in fixed_ips: for ip in fixed_ips:
@ -267,27 +188,13 @@ class Plug(object):
'namespace {2}').format(default_netns_interface, 'namespace {2}').format(default_netns_interface,
netns_interface, netns_interface,
consts.AMPHORA_NAMESPACE)) consts.AMPHORA_NAMESPACE))
interface_file_path = util.get_network_interface_file(netns_interface) interface_file_path = self._osutils.get_network_interface_file(
netns_interface)
# write interface file self._osutils.write_port_interface_file(
netns_interface=netns_interface,
# If we are using a consolidated interfaces file, just append fixed_ips=fixed_ips,
# otherwise clear the per interface file as we are rewriting it mtu=mtu,
# TODO(johnsom): We need a way to clean out old interfaces records interface_file_path=interface_file_path)
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)
# Update the list of interfaces to add to the namespace # Update the list of interfaces to add to the namespace
self._update_plugged_interfaces_file(netns_interface, mac_address) self._update_plugged_interfaces_file(netns_interface, mac_address)
@ -299,8 +206,8 @@ class Plug(object):
net_ns_fd=consts.AMPHORA_NAMESPACE, net_ns_fd=consts.AMPHORA_NAMESPACE,
IFLA_IFNAME=netns_interface) IFLA_IFNAME=netns_interface)
self._bring_if_down(netns_interface) self._osutils._bring_if_down(netns_interface)
self._bring_if_up(netns_interface, 'network') self._osutils._bring_if_up(netns_interface, 'network')
return flask.make_response(flask.jsonify(dict( return flask.make_response(flask.jsonify(dict(
message="OK", message="OK",
@ -318,31 +225,6 @@ class Plug(object):
response=flask.make_response(flask.jsonify(dict( response=flask.make_response(flask.jsonify(dict(
details="No suitable network interface found")), 404)) 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): def _update_plugged_interfaces_file(self, interface, mac_address):
# write interfaces to plugged_interfaces file and prevent duplicates # write interfaces to plugged_interfaces file and prevent duplicates
plug_inf_file = consts.PLUGGED_INTERFACES 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 certificate_update
from octavia.amphorae.backends.agent.api_server import keepalived 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 listener
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import plug from octavia.amphorae.backends.agent.api_server import plug
PATH_PREFIX = '/' + api_server.VERSION PATH_PREFIX = '/' + api_server.VERSION
@ -43,9 +44,11 @@ def register_app_error_handler(app):
class Server(object): class Server(object):
def __init__(self): def __init__(self):
self.app = flask.Flask(__name__) self.app = flask.Flask(__name__)
self._osutils = osutils.BaseOS.get_os_util()
self._keepalived = keepalived.Keepalived() self._keepalived = keepalived.Keepalived()
self._listener = listener.Listener() 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) register_app_error_handler(self.app)
@ -119,10 +122,10 @@ class Server(object):
return self._listener.delete_listener(listener_id) return self._listener.delete_listener(listener_id)
def get_details(self): def get_details(self):
return amphora_info.compile_amphora_details() return self._amphora_info.compile_amphora_details()
def get_info(self): def get_info(self):
return amphora_info.compile_amphora_info() return self._amphora_info.compile_amphora_info()
def get_all_listeners_status(self): def get_all_listeners_status(self):
return self._listener.get_all_listeners_status() return self._listener.get_all_listeners_status()
@ -179,4 +182,4 @@ class Server(object):
return self._keepalived.manager_keepalived_service(action) return self._keepalived.manager_keepalived_service(action)
def get_interface(self, ip_addr): 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 # Generated by Octavia agent
auto {{ interface }} {{ interface }}:0 auto {{ interface }} {{ interface }}:0
{%- if vrrp_ip %} {%- if vrrp_ip %}
iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static
address {{ vrrp_ip }} 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 }} down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endfor %} {%- endfor %}
{%- else %} {%- else %}
iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }} iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %} {%- 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 # 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' 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 # 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 # Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ExecStartPre=-/sbin/ip netns exec {{ amphora_nsname }} ifup -a 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 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 }} 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 # 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 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 # 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 # Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true 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" \ start-stop-daemon --start --pidfile "$PIDFILE" \
--exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \ --exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \

View File

@ -39,9 +39,14 @@ pre-start script
# interfaces # interfaces
sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted || true 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 # 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 # Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true 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 end script
script script

View File

@ -125,13 +125,6 @@ def is_listener_running(listener_id):
os.path.join('/proc', get_haproxy_pid(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(): def get_os_init_system():
if os.path.exists(consts.INIT_PROC_COMM_PATH): if os.path.exists(consts.INIT_PROC_COMM_PATH):
with open(consts.INIT_PROC_COMM_PATH, 'r') as init_comm: with open(consts.INIT_PROC_COMM_PATH, 'r') as init_comm:

View File

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

View File

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

View File

@ -366,3 +366,8 @@ KEEPALIVED_UPSTART = 'octavia-keepalived.conf'
# Authentication # Authentication
KEYSTONE = 'keystone' KEYSTONE = 'keystone'
NOAUTH = 'noauth' 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.') message = _('Failed to delete server group object.')
class InvalidAmphoraOperatingSystem(OctaviaException):
message = _('Invalid amphora operating system: %(os_name)s')
class QuotaException(APIException): class QuotaException(APIException):
msg = _('Quota has been met.') msg = _('Quota has been met.')
code = 403 code = 403

View File

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

View File

@ -28,6 +28,9 @@ class OpenFixture(fixtures.Fixture):
def _setUp(self): def _setUp(self):
self.mock_open = mock.mock_open(read_data=self.contents) 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 self._orig_open = open
def replacement_open(name, *args, **kwargs): 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 mock
import netifaces import netifaces
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import plug from octavia.amphorae.backends.agent.api_server import plug
import octavia.tests.unit.base as base import octavia.tests.unit.base as base
@ -36,7 +37,8 @@ class TestPlug(base.TestCase):
def setUp(self): def setUp(self):
super(TestPlug, self).setUp() super(TestPlug, self).setUp()
self.mock_netifaces = mock.patch.object(plug, "netifaces").start() 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) self.addCleanup(self.mock_netifaces.stop)
# Set up our fake interface # 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()) interface = self.test_plug._interface_by_mac(FAKE_MAC_ADDRESS.upper())
self.assertEqual(FAKE_INTERFACE, interface) 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('pyroute2.NSPopen')
@mock.patch.object(plug, "flask") @mock.patch.object(plug, "flask")
@mock.patch('pyroute2.IPRoute') @mock.patch('pyroute2.IPRoute')
@ -146,9 +157,10 @@ class TestPlug(base.TestCase):
class TestPlugNetwork(base.TestCase): class TestPlugNetwork(base.TestCase):
def setUp(self): def setUp(self):
super(TestPlugNetwork, self).setUp() 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' netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2' FIXED_IP = '192.0.2.2'
BROADCAST = '192.0.2.255' BROADCAST = '192.0.2.255'
@ -177,8 +189,9 @@ class TestPlugNetwork(base.TestCase):
'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n' 'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n') 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n')
text = self.test_plug._generate_network_file_text(netns_interface, template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2')
fixed_ips, MTU) 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, expected_text = format_text.format(netns_interface=netns_interface,
fixed_ip=FIXED_IP, fixed_ip=FIXED_IP,
broadcast=BROADCAST, 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)) utils.ip_port_str('127.0.0.1', 8080))
self.assertEqual("[::1]:8080", self.assertEqual("[::1]:8080",
utils.ip_port_str('::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.