diff --git a/config.yaml b/config.yaml index 501aff83..41e37e5b 100644 --- a/config.yaml +++ b/config.yaml @@ -41,8 +41,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 @@ -52,6 +54,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 + :. + 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: diff --git a/hooks/charmhelpers/contrib/openstack/templates/zeromq b/hooks/charmhelpers/contrib/openstack/templates/zeromq new file mode 100644 index 00000000..0695eef1 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/zeromq @@ -0,0 +1,14 @@ +{% if zmq_host -%} +# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }}) +rpc_backend = zmq +rpc_zmq_host = {{ zmq_host }} +{% if zmq_redis_address -%} +rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis +matchmaker_heartbeat_freq = 15 +matchmaker_heartbeat_ttl = 30 +[matchmaker_redis] +host = {{ zmq_redis_address }} +{% else -%} +rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing +{% endif -%} +{% endif -%} diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py index 61e5a679..4c6febf5 100644 --- a/hooks/neutron_ovs_context.py +++ b/hooks/neutron_ovs_context.py @@ -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 diff --git a/hooks/neutron_ovs_utils.py b/hooks/neutron_ovs_utils.py index cd96257b..4aa87d41 100644 --- a/hooks/neutron_ovs_utils.py +++ b/hooks/neutron_ovs_utils.py @@ -39,6 +39,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, { @@ -50,8 +52,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(): diff --git a/templates/icehouse/ml2_conf.ini b/templates/icehouse/ml2_conf.ini index 56be2052..f2aa23e3 100644 --- a/templates/icehouse/ml2_conf.ini +++ b/templates/icehouse/ml2_conf.ini @@ -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 -%} diff --git a/templates/icehouse/neutron.conf b/templates/icehouse/neutron.conf index 964f6dce..c7af465a 100644 --- a/templates/icehouse/neutron.conf +++ b/templates/icehouse/neutron.conf @@ -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 }} diff --git a/templates/os-charm-phy-nic-mtu.conf b/templates/os-charm-phy-nic-mtu.conf new file mode 100644 index 00000000..06d1967b --- /dev/null +++ b/templates/os-charm-phy-nic-mtu.conf @@ -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 \ No newline at end of file diff --git a/unit_tests/test_neutron_ovs_context.py b/unit_tests/test_neutron_ovs_context.py index ec51d54a..a7da378b 100644 --- a/unit_tests/test_neutron_ovs_context.py +++ b/unit_tests/test_neutron_ovs_context.py @@ -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() diff --git a/unit_tests/test_neutron_ovs_utils.py b/unit_tests/test_neutron_ovs_utils.py index d5555517..5d0a304b 100644 --- a/unit_tests/test_neutron_ovs_utils.py +++ b/unit_tests/test_neutron_ovs_utils.py @@ -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])