[dosaboy,r=james-page] Support mtu from neutron-api, add support for bridge mapping configuration.

This commit is contained in:
James Page 2015-03-19 17:00:09 +00:00
commit 9af23e724c
13 changed files with 349 additions and 64 deletions

View File

@ -25,8 +25,10 @@ options:
type: string
default:
description: |
The data port will be added to br-data and will allow usage of flat or VLAN
network types
Space-delimited list of bridge:port mappings. Ports will be added to
their corresponding bridge. The bridges will allow usage of flat or
VLAN network types with Neutron and should match this defined in
bridge-mappings.
disable-security-groups:
type: boolean
default: false
@ -36,6 +38,17 @@ options:
.
BE CAREFUL - this option allows you to disable all port level security within
an OpenStack cloud.
bridge-mappings:
type: string
default: 'physnet1:br-data'
description: |
Space-delimited list of ML2 data bridge mappings with format
<provider>:<bridge>.
vlan-ranges:
type: string
default: "physnet1:1000:2000"
description: |
Space-delimited list of network provider vlan id ranges.
# Network configuration options
# by default all access is over 'private-address'
os-data-network:

View File

@ -16,6 +16,7 @@
import json
import os
import re
import time
from base64 import b64decode
from subprocess import check_call
@ -48,6 +49,8 @@ from charmhelpers.core.hookenv import (
from charmhelpers.core.sysctl import create as sysctl_create
from charmhelpers.core.host import (
list_nics,
get_nic_hwaddr,
mkdir,
write_file,
)
@ -65,12 +68,18 @@ from charmhelpers.contrib.hahelpers.apache import (
from charmhelpers.contrib.openstack.neutron import (
neutron_plugin_attribute,
)
from charmhelpers.contrib.openstack.ip import (
resolve_address,
INTERNAL,
)
from charmhelpers.contrib.network.ip import (
get_address_in_network,
get_ipv4_addr,
get_ipv6_addr,
get_netmask_for_address,
format_ipv6_addr,
is_address_in_network,
is_bridge_member,
)
from charmhelpers.contrib.openstack.utils import get_host_ip
@ -727,7 +736,14 @@ class ApacheSSLContext(OSContextGenerator):
'endpoints': [],
'ext_ports': []}
for cn in self.canonical_names():
cns = self.canonical_names()
if cns:
for cn in cns:
self.configure_cert(cn)
else:
# Expect cert/key provided in config (currently assumed that ca
# uses ip for cn)
cn = resolve_address(endpoint_type=INTERNAL)
self.configure_cert(cn)
addresses = self.get_network_addresses()
@ -883,6 +899,48 @@ class NeutronContext(OSContextGenerator):
return ctxt
class NeutronPortContext(OSContextGenerator):
NIC_PREFIXES = ['eth', 'bond']
def resolve_ports(self, ports):
"""Resolve NICs not yet bound to bridge(s)
If hwaddress provided then returns resolved hwaddress otherwise NIC.
"""
if not ports:
return None
hwaddr_to_nic = {}
hwaddr_to_ip = {}
for nic in list_nics(self.NIC_PREFIXES):
hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False)
addresses += get_ipv6_addr(iface=nic, fatal=False)
hwaddr_to_ip[hwaddr] = addresses
resolved = []
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
for entry in ports:
if re.match(mac_regex, entry):
# NIC is in known NICs and does NOT hace an IP address
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
# If the nic is part of a bridge then don't use it
if is_bridge_member(hwaddr_to_nic[entry]):
continue
# Entry is a MAC address for a valid interface that doesn't
# have an IP address assigned yet.
resolved.append(hwaddr_to_nic[entry])
else:
# If the passed entry is not a MAC address, assume it's a valid
# interface, and that the user put it there on purpose (we can
# trust it to be the real external network).
resolved.append(entry)
return resolved
class OSConfigFlagContext(OSContextGenerator):
"""Provides support for user-defined config flags.

View File

@ -16,6 +16,7 @@
# Various utilies for dealing with Neutron and the renaming from Quantum.
import six
from subprocess import check_output
from charmhelpers.core.hookenv import (
@ -237,3 +238,72 @@ def network_manager():
else:
# ensure accurate naming for all releases post-H
return 'neutron'
def parse_mappings(mappings):
parsed = {}
if mappings:
mappings = mappings.split(' ')
for m in mappings:
p = m.partition(':')
if p[1] == ':':
parsed[p[0].strip()] = p[2].strip()
return parsed
def parse_bridge_mappings(mappings):
"""Parse bridge mappings.
Mappings must be a space-delimited list of provider:bridge mappings.
Returns dict of the form {provider:bridge}.
"""
return parse_mappings(mappings)
def parse_data_port_mappings(mappings, default_bridge='br-data'):
"""Parse data port mappings.
Mappings must be a space-delimited list of bridge:port mappings.
Returns dict of the form {bridge:port}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
if not mappings:
return {}
# For backwards-compatibility we need to support port-only provided in
# config.
_mappings = {default_bridge: mappings.split(' ')[0]}
bridges = _mappings.keys()
ports = _mappings.values()
if len(set(bridges)) != len(bridges):
raise Exception("It is not allowed to have more than one port "
"configured on the same bridge")
if len(set(ports)) != len(ports):
raise Exception("It is not allowed to have the same port configured "
"on more than one bridge")
return _mappings
def parse_vlan_range_mappings(mappings):
"""Parse vlan range mappings.
Mappings must be a space-delimited list of provider:start:end mappings.
Returns dict of the form {provider: (start, end)}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
return {}
mappings = {}
for p, r in six.iteritems(_mappings):
mappings[p] = tuple(r.split(':'))
return mappings

View File

@ -566,3 +566,29 @@ class Hooks(object):
def charm_dir():
"""Return the root directory of the current charm"""
return os.environ.get('CHARM_DIR')
@cached
def action_get(key=None):
"""Gets the value of an action parameter, or all key/value param pairs"""
cmd = ['action-get']
if key is not None:
cmd.append(key)
cmd.append('--format=json')
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
return action_data
def action_set(values):
"""Sets the values to be returned after the action finishes"""
cmd = ['action-set']
for k, v in list(values.items()):
cmd.append('{}={}'.format(k, v))
subprocess.check_call(cmd)
def action_fail(message):
"""Sets the action status to failed and sets the error message.
The results set by action_set are preserved."""
subprocess.check_call(['action-fail', message])

View File

@ -339,12 +339,16 @@ def lsb_release():
def pwgen(length=None):
"""Generate a random pasword."""
if length is None:
# A random length is ok to use a weak PRNG
length = random.choice(range(35, 45))
alphanumeric_chars = [
l for l in (string.ascii_letters + string.digits)
if l not in 'l0QD1vAEIOUaeiou']
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
# actual password
random_generator = random.SystemRandom()
random_chars = [
random.choice(alphanumeric_chars) for _ in range(length)]
random_generator.choice(alphanumeric_chars) for _ in range(length)]
return(''.join(random_chars))

View File

@ -139,7 +139,7 @@ class MysqlRelation(RelationContext):
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'user', 'password', 'database']
super(HttpRelation).__init__(self, *args, **kwargs)
RelationContext.__init__(self, *args, **kwargs)
class HttpRelation(RelationContext):
@ -154,7 +154,7 @@ class HttpRelation(RelationContext):
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'port']
super(HttpRelation).__init__(self, *args, **kwargs)
RelationContext.__init__(self, *args, **kwargs)
def provide_data(self):
return {

View File

@ -5,17 +5,25 @@ from charmhelpers.core.hookenv import (
config,
unit_get,
)
from charmhelpers.core.host import list_nics, get_nic_hwaddr
from charmhelpers.core.strutils import bool_from_string
from charmhelpers.contrib.openstack import context
from charmhelpers.core.host import service_running, service_start
from charmhelpers.core.host import (
service_running,
service_start,
service_restart,
)
from charmhelpers.contrib.network.ovs import add_bridge, add_bridge_port
from charmhelpers.contrib.openstack.utils import get_host_ip
from charmhelpers.contrib.network.ip import get_address_in_network
import re
from charmhelpers.contrib.openstack.neutron import (
parse_bridge_mappings,
parse_data_port_mappings,
parse_vlan_range_mappings,
)
from charmhelpers.core.host import (
get_nic_hwaddr,
)
OVS_BRIDGE = 'br-int'
DATA_BRIDGE = 'br-data'
def _neutron_api_settings():
@ -27,25 +35,48 @@ def _neutron_api_settings():
'l2_population': True,
'overlay_network_type': 'gre',
}
for rid in relation_ids('neutron-plugin-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
if 'l2-population' not in rdata:
continue
neutron_settings = {
'l2_population': bool_from_string(rdata['l2-population']),
'overlay_network_type': rdata['overlay-network-type'],
'neutron_security_groups': bool_from_string(
rdata['neutron-security-groups']
),
}
if 'l2-population' in rdata:
neutron_settings.update({
'l2_population': bool_from_string(rdata['l2-population']),
'overlay_network_type': rdata['overlay-network-type'],
'neutron_security_groups':
bool_from_string(rdata['neutron-security-groups'])
})
# Override with configuration if set to true
if config('disable-security-groups'):
neutron_settings['neutron_security_groups'] = False
return neutron_settings
net_dev_mtu = rdata.get('network-device-mtu')
if net_dev_mtu:
neutron_settings['network_device_mtu'] = net_dev_mtu
return neutron_settings
class DataPortContext(context.NeutronPortContext):
def __call__(self):
ports = config('data-port')
if ports:
portmap = parse_data_port_mappings(ports)
ports = portmap.values()
resolved = self.resolve_ports(ports)
normalized = {get_nic_hwaddr(port): port for port in resolved
if port not in ports}
normalized.update({port: port for port in resolved
if port in ports})
if resolved:
return {bridge: normalized[port] for bridge, port in
portmap.iteritems() if port in normalized.keys()}
return None
class OVSPluginContext(context.NeutronContext):
interfaces = []
@ -62,31 +93,23 @@ class OVSPluginContext(context.NeutronContext):
neutron_api_settings = _neutron_api_settings()
return neutron_api_settings['neutron_security_groups']
def get_data_port(self):
data_ports = config('data-port')
if not data_ports:
return None
hwaddrs = {}
for nic in list_nics(['eth', 'bond']):
hwaddrs[get_nic_hwaddr(nic).lower()] = nic
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
for entry in data_ports.split():
entry = entry.strip().lower()
if re.match(mac_regex, entry):
if entry in hwaddrs:
return hwaddrs[entry]
else:
return entry
return None
def _ensure_bridge(self):
if not service_running('openvswitch-switch'):
service_start('openvswitch-switch')
add_bridge(OVS_BRIDGE)
add_bridge(DATA_BRIDGE)
data_port = self.get_data_port()
if data_port:
add_bridge_port(DATA_BRIDGE, data_port, promisc=True)
portmaps = DataPortContext()()
bridgemaps = parse_bridge_mappings(config('bridge-mappings'))
for provider, br in bridgemaps.iteritems():
add_bridge(br)
if not portmaps or br not in portmaps:
continue
add_bridge_port(br, portmaps[br], promisc=True)
service_restart('os-charm-phy-nic-mtu')
def ovs_ctxt(self):
# In addition to generating config context, ensure the OVS service
@ -112,4 +135,40 @@ class OVSPluginContext(context.NeutronContext):
ovs_ctxt['use_syslog'] = conf['use-syslog']
ovs_ctxt['verbose'] = conf['verbose']
ovs_ctxt['debug'] = conf['debug']
net_dev_mtu = neutron_api_settings.get('network_device_mtu')
if net_dev_mtu:
# neutron.conf
ovs_ctxt['network_device_mtu'] = net_dev_mtu
# ml2 conf
ovs_ctxt['veth_mtu'] = net_dev_mtu
mappings = config('bridge-mappings')
if mappings:
ovs_ctxt['bridge_mappings'] = mappings
vlan_ranges = config('vlan-ranges')
vlan_range_mappings = parse_vlan_range_mappings(config('vlan-ranges'))
if vlan_ranges:
providers = vlan_range_mappings.keys()
ovs_ctxt['network_providers'] = ' '.join(providers)
ovs_ctxt['vlan_ranges'] = vlan_ranges
return ovs_ctxt
class PhyNICMTUContext(DataPortContext):
"""Context used to apply settings to neutron data-port devices"""
def __call__(self):
ctxt = {}
mappings = super(PhyNICMTUContext, self).__call__()
if mappings and mappings.values():
ports = mappings.values()
neutron_api_settings = _neutron_api_settings()
mtu = neutron_api_settings.get('network_device_mtu')
if mtu:
ctxt['devs'] = '\\n'.join(ports)
ctxt['mtu'] = mtu
return ctxt

View File

@ -13,6 +13,8 @@ NEUTRON_CONF_DIR = "/etc/neutron"
NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR
NEUTRON_DEFAULT = '/etc/default/neutron-server'
ML2_CONF = '%s/plugins/ml2/ml2_conf.ini' % NEUTRON_CONF_DIR
PHY_NIC_MTU_CONF = '/etc/init/os-charm-phy-nic-mtu.conf'
TEMPLATES = 'templates/'
BASE_RESOURCE_MAP = OrderedDict([
(NEUTRON_CONF, {
@ -24,8 +26,11 @@ BASE_RESOURCE_MAP = OrderedDict([
'services': ['neutron-plugin-openvswitch-agent'],
'contexts': [neutron_ovs_context.OVSPluginContext()],
}),
(PHY_NIC_MTU_CONF, {
'services': ['os-charm-phy-nic-mtu'],
'contexts': [neutron_ovs_context.PhyNICMTUContext()],
}),
])
TEMPLATES = 'templates/'
def determine_packages():

View File

@ -16,19 +16,22 @@ tunnel_id_ranges = 1:1000
vni_ranges = 1001:2000
[ml2_type_vlan]
network_vlan_ranges = physnet1:1000:2000
network_vlan_ranges = {{ vlan_ranges }}
[ml2_type_flat]
flat_networks = physnet1
flat_networks = {{ network_providers }}
[ovs]
enable_tunneling = True
local_ip = {{ local_ip }}
bridge_mappings = physnet1:br-data
bridge_mappings = {{ bridge_mappings }}
[agent]
tunnel_types = {{ overlay_network_type }}
l2_population = {{ l2_population }}
{% if veth_mtu -%}
veth_mtu = {{ veth_mtu }}
{% endif %}
[securitygroup]
{% if neutron_security_groups -%}

View File

@ -1,4 +1,4 @@
# grizzly
# icehouse
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
@ -12,6 +12,7 @@ state_path = /var/lib/neutron
lock_path = $state_path/lock
bind_host = 0.0.0.0
bind_port = 9696
network_device_mtu = {{ network_device_mtu }}
{% if core_plugin -%}
core_plugin = {{ core_plugin }}

View File

@ -0,0 +1,22 @@
description "Enabling Quantum external networking port"
start on runlevel [2345]
task
script
devs="{{ devs }}"
mtu="{{ mtu }}"
tmpfile=`mktemp`
echo $devs > $tmpfile
if [ -n "$mtu" ]; then
while read -r dev; do
[ -n "$dev" ] || continue
rc=0
# Try all devices before exiting with error
ip link set $dev mtu $mtu || rc=$?
done < $tmpfile
rm $tmpfile
[ $rc = 0 ] || exit $rc
fi
end script

View File

@ -14,8 +14,6 @@ TO_PATCH = [
'service_running',
'service_start',
'get_host_ip',
'get_nic_hwaddr',
'list_nics',
]
@ -32,34 +30,45 @@ class OVSPluginContextTest(CharmTestCase):
def tearDown(self):
super(OVSPluginContextTest, self).tearDown()
def test_data_port_name(self):
self.test_config.set('data-port', 'em1')
self.assertEquals(context.OVSPluginContext().get_data_port(), 'em1')
@patch('charmhelpers.contrib.openstack.context.NeutronPortContext.'
'resolve_ports')
def test_data_port_name(self, mock_resolve_ports):
self.test_config.set('data-port', 'br-data:em1')
mock_resolve_ports.side_effect = lambda ports: ports
self.assertEquals(context.DataPortContext()(),
{'br-data': 'em1'})
def test_data_port_mac(self):
@patch.object(context, 'get_nic_hwaddr')
@patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr')
@patch('charmhelpers.contrib.openstack.context.list_nics')
def test_data_port_mac(self, list_nics, get_nic_hwaddr, get_nic_hwaddr2):
machine_machs = {
'em1': 'aa:aa:aa:aa:aa:aa',
'eth0': 'bb:bb:bb:bb:bb:bb',
}
get_nic_hwaddr2.side_effect = lambda nic: machine_machs[nic]
absent_mac = "cc:cc:cc:cc:cc:cc"
config_macs = "%s %s" % (absent_mac, machine_machs['em1'])
config_macs = ("br-d1:%s br-d2:%s" %
(absent_mac, machine_machs['em1']))
self.test_config.set('data-port', config_macs)
list_nics.return_value = machine_machs.keys()
get_nic_hwaddr.side_effect = lambda nic: machine_machs[nic]
self.assertEquals(context.DataPortContext()(),
{'br-d2': 'em1'})
def get_hwaddr(eth):
return machine_machs[eth]
self.get_nic_hwaddr.side_effect = get_hwaddr
self.list_nics.return_value = machine_machs.keys()
self.assertEquals(context.OVSPluginContext().get_data_port(), 'em1')
@patch('charmhelpers.contrib.openstack.context.NeutronPortContext.'
'resolve_ports')
def test_ensure_bridge_data_port_present(self, mock_resolve_ports):
self.test_config.set('data-port', 'br-data:em1')
self.test_config.set('bridge-mappings', 'phybr1:br-data')
@patch.object(context.OVSPluginContext, 'get_data_port')
def test_ensure_bridge_data_port_present(self, get_data_port):
def add_port(bridge, port, promisc):
if bridge == 'br-data' and port == 'em1' and promisc is True:
self.bridge_added = True
return
self.bridge_added = False
get_data_port.return_value = 'em1'
mock_resolve_ports.side_effect = lambda ports: ports
self.add_bridge_port.side_effect = add_port
context.OVSPluginContext()._ensure_bridge()
self.assertEquals(self.bridge_added, True)
@ -90,6 +99,7 @@ class OVSPluginContextTest(CharmTestCase):
self.relation_ids.return_value = ['rid2']
self.test_relation.set({'neutron-security-groups': 'True',
'l2-population': 'True',
'network-device-mtu': 1500,
'overlay-network-type': 'gre',
})
self.get_host_ip.return_value = '127.0.0.15'
@ -100,6 +110,8 @@ class OVSPluginContextTest(CharmTestCase):
'neutron_security_groups': True,
'verbose': True,
'local_ip': '127.0.0.15',
'network_device_mtu': 1500,
'veth_mtu': 1500,
'config': 'neutron.randomconfig',
'use_syslog': True,
'network_manager': 'neutron',
@ -109,6 +121,9 @@ class OVSPluginContextTest(CharmTestCase):
'neutron_url': 'https://127.0.0.13:9696',
'l2_population': True,
'overlay_network_type': 'gre',
'network_providers': 'physnet1',
'bridge_mappings': 'physnet1:br-data',
'vlan_ranges': 'physnet1:1000:2000',
}
self.assertEquals(expect, napi_ctxt())
self.service_start.assertCalled()
@ -133,6 +148,7 @@ class OVSPluginContextTest(CharmTestCase):
return "neutron.randomdriver"
if section == "config":
return "neutron.randomconfig"
_npa.side_effect = mock_npa
_config.return_value = 'ovs'
_unit_get.return_value = '127.0.0.13'
@ -143,6 +159,7 @@ class OVSPluginContextTest(CharmTestCase):
self.relation_ids.return_value = ['rid2']
self.test_relation.set({'neutron-security-groups': 'True',
'l2-population': 'True',
'network-device-mtu': 1500,
'overlay-network-type': 'gre',
})
self.get_host_ip.return_value = '127.0.0.15'
@ -153,6 +170,8 @@ class OVSPluginContextTest(CharmTestCase):
'neutron_security_groups': False,
'verbose': True,
'local_ip': '127.0.0.15',
'veth_mtu': 1500,
'network_device_mtu': 1500,
'config': 'neutron.randomconfig',
'use_syslog': True,
'network_manager': 'neutron',
@ -162,6 +181,9 @@ class OVSPluginContextTest(CharmTestCase):
'neutron_url': 'https://127.0.0.13:9696',
'l2_population': True,
'overlay_network_type': 'gre',
'network_providers': 'physnet1',
'bridge_mappings': 'physnet1:br-data',
'vlan_ranges': 'physnet1:1000:2000',
}
self.assertEquals(expect, napi_ctxt())
self.service_start.assertCalled()

View File

@ -71,7 +71,8 @@ class TestNeutronOVSUtils(CharmTestCase):
templating.OSConfigRenderer.side_effect = _mock_OSConfigRenderer
_regconfs = nutils.register_configs()
confs = ['/etc/neutron/neutron.conf',
'/etc/neutron/plugins/ml2/ml2_conf.ini']
'/etc/neutron/plugins/ml2/ml2_conf.ini',
'/etc/init/os-charm-phy-nic-mtu.conf']
self.assertItemsEqual(_regconfs.configs, confs)
def test_resource_map(self):
@ -85,8 +86,9 @@ class TestNeutronOVSUtils(CharmTestCase):
expect = OrderedDict([
(nutils.NEUTRON_CONF, ['neutron-plugin-openvswitch-agent']),
(ML2CONF, ['neutron-plugin-openvswitch-agent']),
(nutils.PHY_NIC_MTU_CONF, ['os-charm-phy-nic-mtu'])
])
self.assertTrue(len(expect) == len(_restart_map))
self.assertEqual(expect, _restart_map)
for item in _restart_map:
self.assertTrue(item in _restart_map)
self.assertTrue(expect[item] == _restart_map[item])