diff --git a/config.yaml b/config.yaml index 41699d49..94f19c67 100644 --- a/config.yaml +++ b/config.yaml @@ -60,3 +60,12 @@ options: . This network will be used for tenant network traffic in overlay networks. + ext-port: + type: string + default: + description: | + A space-separated list of external ports to use for routing of instance + traffic to the external public network. Valid values are either MAC + addresses (in which case only MAC addresses for interfaces without an IP + address already assigned will be used), or interfaces (eth0) + diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 45e65790..dd51bfbb 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -320,14 +320,15 @@ def db_ssl(rdata, ctxt, ssl_dir): class IdentityServiceContext(OSContextGenerator): - interfaces = ['identity-service'] - def __init__(self, service=None, service_user=None): + def __init__(self, service=None, service_user=None, rel_name='identity-service'): self.service = service self.service_user = service_user + self.rel_name = rel_name + self.interfaces = [self.rel_name] def __call__(self): - log('Generating template context for identity-service', level=DEBUG) + log('Generating template context for ' + self.rel_name, level=DEBUG) ctxt = {} if self.service and self.service_user: @@ -341,7 +342,7 @@ class IdentityServiceContext(OSContextGenerator): ctxt['signing_dir'] = cachedir - for rid in relation_ids('identity-service'): + for rid in relation_ids(self.rel_name): for unit in related_units(rid): rdata = relation_get(rid=rid, unit=unit) serv_host = rdata.get('service_host') diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py index 46ae4e7d..4ca582f9 100644 --- a/hooks/neutron_ovs_context.py +++ b/hooks/neutron_ovs_context.py @@ -1,25 +1,23 @@ +import os +import uuid from charmhelpers.core.hookenv import ( config, + relation_get, + relation_ids, + related_units, unit_get, ) +from charmhelpers.contrib.openstack.ip import resolve_address from charmhelpers.contrib.openstack import context -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 from charmhelpers.contrib.openstack.context import ( + OSContextGenerator, NeutronAPIContext, - DataPortContext, ) from charmhelpers.contrib.openstack.neutron import ( - parse_bridge_mappings, parse_vlan_range_mappings, ) -OVS_BRIDGE = 'br-int' class OVSPluginContext(context.NeutronContext): @@ -40,24 +38,6 @@ class OVSPluginContext(context.NeutronContext): neutron_api_settings = NeutronAPIContext()() return neutron_api_settings['neutron_security_groups'] - def _ensure_bridge(self): - if not service_running('openvswitch-switch'): - service_start('openvswitch-switch') - - add_bridge(OVS_BRIDGE) - - 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 # is running and the OVS bridge exists. Also need to ensure @@ -66,8 +46,6 @@ class OVSPluginContext(context.NeutronContext): if not ovs_ctxt: return {} - self._ensure_bridge() - conf = config() ovs_ctxt['local_ip'] = \ get_address_in_network(config('os-data-network'), @@ -75,6 +53,7 @@ class OVSPluginContext(context.NeutronContext): neutron_api_settings = NeutronAPIContext()() ovs_ctxt['neutron_security_groups'] = self.neutron_security_groups ovs_ctxt['l2_population'] = neutron_api_settings['l2_population'] + ovs_ctxt['distributed_routing'] = neutron_api_settings['enable_dvr'] ovs_ctxt['overlay_network_type'] = \ neutron_api_settings['overlay_network_type'] # TODO: We need to sort out the syslog and debug/verbose options as a @@ -102,3 +81,62 @@ class OVSPluginContext(context.NeutronContext): ovs_ctxt['vlan_ranges'] = vlan_ranges return ovs_ctxt + + +class L3AgentContext(OSContextGenerator): + + def __call__(self): + neutron_api_settings = NeutronAPIContext()() + ctxt = {} + if neutron_api_settings['enable_dvr']: + ctxt['agent_mode'] = 'dvr' + else: + ctxt['agent_mode'] = 'legacy' + return ctxt + + +SHARED_SECRET = "/etc/neutron/secret.txt" + + +def get_shared_secret(): + secret = None + if not os.path.exists(SHARED_SECRET): + secret = str(uuid.uuid4()) + with open(SHARED_SECRET, 'w') as secret_file: + secret_file.write(secret) + else: + with open(SHARED_SECRET, 'r') as secret_file: + secret = secret_file.read().strip() + return secret + + +class DVRSharedSecretContext(OSContextGenerator): + + def __call__(self): + if NeutronAPIContext()()['enable_dvr']: + ctxt = { + 'shared_secret': get_shared_secret(), + 'local_ip': resolve_address(), + } + else: + ctxt = {} + return ctxt + + +class APIIdentityServiceContext(context.IdentityServiceContext): + + def __init__(self): + super(APIIdentityServiceContext, + self).__init__(rel_name='neutron-plugin-api') + + def __call__(self): + ctxt = super(APIIdentityServiceContext, self).__call__() + if not ctxt: + return + for rid in relation_ids('neutron-plugin-api'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + ctxt['region'] = rdata.get('region') + if ctxt['region']: + return ctxt + return ctxt diff --git a/hooks/neutron_ovs_hooks.py b/hooks/neutron_ovs_hooks.py index eb53094d..15323ebf 100755 --- a/hooks/neutron_ovs_hooks.py +++ b/hooks/neutron_ovs_hooks.py @@ -8,6 +8,7 @@ from charmhelpers.core.hookenv import ( config, log, relation_set, + relation_ids, ) from charmhelpers.core.host import ( @@ -15,13 +16,18 @@ from charmhelpers.core.host import ( ) from charmhelpers.fetch import ( - apt_install, apt_update + apt_install, apt_update, apt_purge ) from neutron_ovs_utils import ( + DVR_PACKAGES, + configure_ovs, determine_packages, + determine_dvr_packages, + get_shared_secret, register_configs, restart_map, + use_dvr, ) hooks = Hooks() @@ -37,13 +43,40 @@ def install(): @hooks.hook('neutron-plugin-relation-changed') -@hooks.hook('neutron-plugin-api-relation-changed') @hooks.hook('config-changed') @restart_on_change(restart_map()) def config_changed(): + if determine_dvr_packages(): + apt_update() + apt_install(determine_dvr_packages(), fatal=True) + configure_ovs() CONFIGS.write_all() +@hooks.hook('neutron-plugin-api-relation-changed') +@restart_on_change(restart_map()) +def neutron_plugin_api_changed(): + if use_dvr(): + apt_update() + apt_install(DVR_PACKAGES, fatal=True) + else: + apt_purge(DVR_PACKAGES, fatal=True) + configure_ovs() + CONFIGS.write_all() + # If dvr setting has changed, need to pass that on + for rid in relation_ids('neutron-plugin'): + neutron_plugin_joined(relation_id=rid) + + +@hooks.hook('neutron-plugin-relation-joined') +def neutron_plugin_joined(relation_id=None): + secret = get_shared_secret() if use_dvr() else None + rel_data = { + 'metadata-shared-secret': secret, + } + relation_set(relation_id=relation_id, **rel_data) + + @hooks.hook('amqp-relation-joined') def amqp_joined(relation_id=None): relation_set(relation_id=relation_id, diff --git a/hooks/neutron_ovs_utils.py b/hooks/neutron_ovs_utils.py index 66b8a77b..233e5313 100644 --- a/hooks/neutron_ovs_utils.py +++ b/hooks/neutron_ovs_utils.py @@ -7,12 +7,36 @@ from charmhelpers.contrib.openstack.utils import ( os_release, ) import neutron_ovs_context +from charmhelpers.contrib.network.ovs import ( + add_bridge, + add_bridge_port, + full_restart, +) +from charmhelpers.core.hookenv import ( + config, +) +from charmhelpers.contrib.openstack.neutron import ( + parse_bridge_mappings, +) +from charmhelpers.contrib.openstack.context import ( + ExternalPortContext, + DataPortContext, +) +from charmhelpers.core.host import ( + service_restart, + service_running, +) NOVA_CONF_DIR = "/etc/nova" NEUTRON_CONF_DIR = "/etc/neutron" NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR NEUTRON_DEFAULT = '/etc/default/neutron-server' +NEUTRON_L3_AGENT_CONF = "/etc/neutron/l3_agent.ini" +NEUTRON_FWAAS_CONF = "/etc/neutron/fwaas_driver.ini" ML2_CONF = '%s/plugins/ml2/ml2_conf.ini' % NEUTRON_CONF_DIR +EXT_PORT_CONF = '/etc/init/ext-port.conf' +NEUTRON_METADATA_AGENT_CONF = "/etc/neutron/metadata_agent.ini" +DVR_PACKAGES = ['neutron-l3-agent'] PHY_NIC_MTU_CONF = '/etc/init/os-charm-phy-nic-mtu.conf' TEMPLATES = 'templates/' @@ -31,10 +55,41 @@ BASE_RESOURCE_MAP = OrderedDict([ 'contexts': [context.PhyNICMTUContext()], }), ]) +DVR_RESOURCE_MAP = OrderedDict([ + (NEUTRON_L3_AGENT_CONF, { + 'services': ['neutron-l3-agent'], + 'contexts': [neutron_ovs_context.L3AgentContext()], + }), + (NEUTRON_FWAAS_CONF, { + 'services': ['neutron-l3-agent'], + 'contexts': [neutron_ovs_context.L3AgentContext()], + }), + (EXT_PORT_CONF, { + 'services': ['neutron-l3-agent'], + 'contexts': [context.ExternalPortContext()], + }), + (NEUTRON_METADATA_AGENT_CONF, { + 'services': ['neutron-metadata-agent'], + 'contexts': [neutron_ovs_context.DVRSharedSecretContext(), + neutron_ovs_context.APIIdentityServiceContext()], + }), +]) +TEMPLATES = 'templates/' +INT_BRIDGE = "br-int" +EXT_BRIDGE = "br-ex" +DATA_BRIDGE = 'br-data' + + +def determine_dvr_packages(): + if use_dvr(): + return DVR_PACKAGES + return [] def determine_packages(): - return neutron_plugin_attribute('ovs', 'packages', 'neutron') + pkgs = neutron_plugin_attribute('ovs', 'packages', 'neutron') + pkgs.extend(determine_dvr_packages()) + return pkgs def register_configs(release=None): @@ -52,6 +107,10 @@ def resource_map(): hook execution. ''' resource_map = deepcopy(BASE_RESOURCE_MAP) + if use_dvr(): + resource_map.update(DVR_RESOURCE_MAP) + dvr_services = ['neutron-metadata-agent', 'neutron-l3-agent'] + resource_map[NEUTRON_CONF]['services'] += dvr_services return resource_map @@ -61,3 +120,38 @@ def restart_map(): state. ''' return {k: v['services'] for k, v in resource_map().iteritems()} + + +def configure_ovs(): + if not service_running('openvswitch-switch'): + full_restart() + add_bridge(INT_BRIDGE) + add_bridge(EXT_BRIDGE) + ext_port_ctx = None + if use_dvr(): + ext_port_ctx = ExternalPortContext()() + if ext_port_ctx and ext_port_ctx['ext_port']: + add_bridge_port(EXT_BRIDGE, ext_port_ctx['ext_port']) + + 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) + + # Ensure this runs so that mtu is applied to data-port interfaces if + # provided. + service_restart('os-charm-phy-nic-mtu') + + +def get_shared_secret(): + ctxt = neutron_ovs_context.DVRSharedSecretContext()() + if 'shared_secret' in ctxt: + return ctxt['shared_secret'] + + +def use_dvr(): + return context.NeutronAPIContext()()['enable_dvr'] diff --git a/templates/ext-port.conf b/templates/ext-port.conf new file mode 100644 index 00000000..1fad0a8b --- /dev/null +++ b/templates/ext-port.conf @@ -0,0 +1,16 @@ +description "Enabling Neutron external networking port" + +start on runlevel [2345] + +task + +script + EXT_PORT="{{ ext_port }}" + MTU="{{ ext_port_mtu }}" + if [ -n "$EXT_PORT" ]; then + ip link set $EXT_PORT up + if [ -n "$MTU" ]; then + ip link set $EXT_PORT mtu $MTU + fi + fi +end script diff --git a/templates/juno/fwaas_driver.ini b/templates/juno/fwaas_driver.ini new file mode 100644 index 00000000..e64046dc --- /dev/null +++ b/templates/juno/fwaas_driver.ini @@ -0,0 +1,7 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[fwaas] +driver = neutron.services.firewall.drivers.linux.iptables_fwaas.IptablesFwaasDriver +enabled = True diff --git a/templates/juno/l3_agent.ini b/templates/juno/l3_agent.ini new file mode 100644 index 00000000..8e93c71a --- /dev/null +++ b/templates/juno/l3_agent.ini @@ -0,0 +1,7 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[DEFAULT] +interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver +agent_mode = {{ agent_mode }} diff --git a/templates/juno/metadata_agent.ini b/templates/juno/metadata_agent.ini new file mode 100644 index 00000000..c64d057c --- /dev/null +++ b/templates/juno/metadata_agent.ini @@ -0,0 +1,20 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +# Metadata service seems to cache neutron api url from keystone so trigger +# restart if it changes: {{ quantum_url }} + +[DEFAULT] +auth_url = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/v2.0 +auth_region = {{ region }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +root_helper = sudo neutron-rootwrap /etc/neutron/rootwrap.conf +state_path = /var/lib/neutron +# Gateway runs a metadata API server locally +#nova_metadata_ip = {{ local_ip }} +nova_metadata_port = 8775 +metadata_proxy_shared_secret = {{ shared_secret }} +cache_url = memory://?default_ttl=5 diff --git a/templates/juno/ml2_conf.ini b/templates/juno/ml2_conf.ini new file mode 100644 index 00000000..f798463a --- /dev/null +++ b/templates/juno/ml2_conf.ini @@ -0,0 +1,43 @@ +# juno +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +# Config managed by neutron-openvswitch charm +############################################################################### +[ml2] +type_drivers = gre,vxlan,vlan,flat +tenant_network_types = gre,vxlan,vlan,flat +mechanism_drivers = openvswitch,hyperv,l2population + +[ml2_type_gre] +tunnel_id_ranges = 1:1000 + +[ml2_type_vxlan] +vni_ranges = 1001:2000 + +[ml2_type_vlan] +network_vlan_ranges = {{ vlan_ranges }} + +[ml2_type_flat] +flat_networks = {{ network_providers }} + +[ovs] +enable_tunneling = True +local_ip = {{ local_ip }} +bridge_mappings = {{ bridge_mappings }} + +[agent] +tunnel_types = {{ overlay_network_type }} +l2_population = {{ l2_population }} +enable_distributed_routing = {{ distributed_routing }} +{% if veth_mtu -%} +veth_mtu = {{ veth_mtu }} +{% endif %} + +[securitygroup] +{% if neutron_security_groups -%} +enable_security_group = True +firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver +{% else -%} +enable_security_group = False +{% endif -%} diff --git a/templates/kilo/fwaas_driver.ini b/templates/kilo/fwaas_driver.ini new file mode 100644 index 00000000..b31a5008 --- /dev/null +++ b/templates/kilo/fwaas_driver.ini @@ -0,0 +1,8 @@ +# kilo +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[fwaas] +driver = neutron_fwaas.services.firewall.drivers.linux.iptables_fwaas.IptablesFwaasDriver +enabled = True diff --git a/unit_tests/test_neutron_ovs_context.py b/unit_tests/test_neutron_ovs_context.py index 5423b5ab..72fb5543 100644 --- a/unit_tests/test_neutron_ovs_context.py +++ b/unit_tests/test_neutron_ovs_context.py @@ -1,20 +1,26 @@ from test_utils import CharmTestCase +from test_utils import patch_open from mock import patch import neutron_ovs_context as context import charmhelpers + TO_PATCH = [ + 'resolve_address', 'config', 'unit_get', - 'add_bridge', - 'add_bridge_port', - 'service_running', - 'service_start', - 'service_restart', 'get_host_ip', ] +def fake_context(settings): + def outer(): + def inner(): + return settings + return inner + return outer + + class OVSPluginContextTest(CharmTestCase): def setUp(self): @@ -34,8 +40,10 @@ class OVSPluginContextTest(CharmTestCase): self.test_config.set('data-port', 'br-data:em1') config.side_effect = self.test_config.get mock_resolve_ports.side_effect = lambda ports: ports - self.assertEquals(context.DataPortContext()(), - {'br-data': 'em1'}) + self.assertEquals( + charmhelpers.contrib.openstack.context.DataPortContext()(), + {'br-data': 'em1'} + ) @patch('charmhelpers.contrib.openstack.context.config') @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr') @@ -52,28 +60,10 @@ class OVSPluginContextTest(CharmTestCase): config.side_effect = self.test_config.get list_nics.return_value = machine_machs.keys() get_nic_hwaddr.side_effect = lambda nic: machine_machs[nic] - self.assertEquals(context.DataPortContext()(), - {'br-d2': 'em1'}) - - @patch('charmhelpers.contrib.openstack.context.config') - @patch('charmhelpers.contrib.openstack.context.NeutronPortContext.' - 'resolve_ports') - def test_ensure_bridge_data_port_present(self, mock_resolve_ports, config): - self.test_config.set('data-port', 'br-data:em1') - self.test_config.set('bridge-mappings', 'phybr1:br-data') - config.side_effect = self.test_config.get - - 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 - - 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) + self.assertEquals( + charmhelpers.contrib.openstack.context.DataPortContext()(), + {'br-d2': 'em1'} + ) @patch.object(charmhelpers.contrib.openstack.context, 'relation_get') @patch.object(charmhelpers.contrib.openstack.context, 'relation_ids') @@ -107,14 +97,15 @@ class OVSPluginContextTest(CharmTestCase): 'l2-population': 'True', 'network-device-mtu': 1500, 'overlay-network-type': 'gre', + 'enable-dvr': 'True', } _rget.side_effect = lambda *args, **kwargs: rdata self.get_host_ip.return_value = '127.0.0.15' - self.service_running.return_value = False napi_ctxt = context.OVSPluginContext() expect = { 'neutron_alchemy_flags': {}, 'neutron_security_groups': True, + 'distributed_routing': True, 'verbose': True, 'local_ip': '127.0.0.15', 'network_device_mtu': 1500, @@ -133,7 +124,6 @@ class OVSPluginContextTest(CharmTestCase): 'vlan_ranges': 'physnet1:1000:2000', } self.assertEquals(expect, napi_ctxt()) - self.service_start.assertCalled() @patch.object(charmhelpers.contrib.openstack.context, 'relation_get') @patch.object(charmhelpers.contrib.openstack.context, 'relation_ids') @@ -176,9 +166,9 @@ class OVSPluginContextTest(CharmTestCase): } _rget.side_effect = lambda *args, **kwargs: rdata self.get_host_ip.return_value = '127.0.0.15' - self.service_running.return_value = False napi_ctxt = context.OVSPluginContext() expect = { + 'distributed_routing': False, 'neutron_alchemy_flags': {}, 'neutron_security_groups': False, 'verbose': True, @@ -199,4 +189,95 @@ class OVSPluginContextTest(CharmTestCase): 'vlan_ranges': 'physnet1:1000:2000', } self.assertEquals(expect, napi_ctxt()) - self.service_start.assertCalled() + + +class L3AgentContextTest(CharmTestCase): + + def setUp(self): + super(L3AgentContextTest, self).setUp(context, TO_PATCH) + self.config.side_effect = self.test_config.get + + def tearDown(self): + super(L3AgentContextTest, self).tearDown() + + @patch.object(charmhelpers.contrib.openstack.context, 'relation_get') + @patch.object(charmhelpers.contrib.openstack.context, 'relation_ids') + @patch.object(charmhelpers.contrib.openstack.context, 'related_units') + def test_dvr_enabled(self, _runits, _rids, _rget): + _runits.return_value = ['unit1'] + _rids.return_value = ['rid2'] + rdata = { + 'neutron-security-groups': 'True', + 'enable-dvr': 'True', + 'l2-population': 'True', + 'overlay-network-type': 'vxlan', + 'network-device-mtu': 1500, + } + _rget.side_effect = lambda *args, **kwargs: rdata + self.assertEquals(context.L3AgentContext()(), {'agent_mode': 'dvr'}) + + @patch.object(charmhelpers.contrib.openstack.context, 'relation_get') + @patch.object(charmhelpers.contrib.openstack.context, 'relation_ids') + @patch.object(charmhelpers.contrib.openstack.context, 'related_units') + def test_dvr_disabled(self, _runits, _rids, _rget): + _runits.return_value = ['unit1'] + _rids.return_value = ['rid2'] + rdata = { + 'neutron-security-groups': 'True', + 'enable-dvr': 'False', + 'l2-population': 'True', + 'overlay-network-type': 'vxlan', + 'network-device-mtu': 1500, + } + _rget.side_effect = lambda *args, **kwargs: rdata + self.assertEquals(context.L3AgentContext()(), {'agent_mode': 'legacy'}) + + +class DVRSharedSecretContext(CharmTestCase): + + def setUp(self): + super(DVRSharedSecretContext, self).setUp(context, + TO_PATCH) + self.config.side_effect = self.test_config.get + + @patch('os.path') + @patch('uuid.uuid4') + def test_secret_created_stored(self, _uuid4, _path): + _path.exists.return_value = False + _uuid4.return_value = 'secret_thing' + with patch_open() as (_open, _file): + self.assertEquals(context.get_shared_secret(), + 'secret_thing') + _open.assert_called_with( + context.SHARED_SECRET.format('quantum'), 'w') + _file.write.assert_called_with('secret_thing') + + @patch('os.path') + def test_secret_retrieved(self, _path): + _path.exists.return_value = True + with patch_open() as (_open, _file): + _file.read.return_value = 'secret_thing\n' + self.assertEquals(context.get_shared_secret(), + 'secret_thing') + _open.assert_called_with( + context.SHARED_SECRET.format('quantum'), 'r') + + @patch.object(context, 'NeutronAPIContext') + @patch.object(context, 'get_shared_secret') + def test_shared_secretcontext_dvr(self, _shared_secret, + _NeutronAPIContext): + _NeutronAPIContext.side_effect = fake_context({'enable_dvr': True}) + _shared_secret.return_value = 'secret_thing' + self.resolve_address.return_value = '10.0.0.10' + self.assertEquals(context.DVRSharedSecretContext()(), + {'shared_secret': 'secret_thing', + 'local_ip': '10.0.0.10'}) + + @patch.object(context, 'NeutronAPIContext') + @patch.object(context, 'get_shared_secret') + def test_shared_secretcontext_nodvr(self, _shared_secret, + _NeutronAPIContext): + _NeutronAPIContext.side_effect = fake_context({'enable_dvr': False}) + _shared_secret.return_value = 'secret_thing' + self.resolve_address.return_value = '10.0.0.10' + self.assertEquals(context.DVRSharedSecretContext()(), {}) diff --git a/unit_tests/test_neutron_ovs_hooks.py b/unit_tests/test_neutron_ovs_hooks.py index d473086b..d651d00f 100644 --- a/unit_tests/test_neutron_ovs_hooks.py +++ b/unit_tests/test_neutron_ovs_hooks.py @@ -2,7 +2,6 @@ from mock import MagicMock, patch, call from test_utils import CharmTestCase - with patch('charmhelpers.core.hookenv.config') as config: config.return_value = 'neutron' import neutron_ovs_utils as utils @@ -21,11 +20,17 @@ utils.restart_map = _map TO_PATCH = [ 'apt_update', 'apt_install', + 'apt_purge', 'config', 'CONFIGS', 'determine_packages', + 'determine_dvr_packages', + 'get_shared_secret', 'log', + 'relation_ids', 'relation_set', + 'configure_ovs', + 'use_dvr', ] NEUTRON_CONF_DIR = "/etc/neutron" @@ -56,6 +61,36 @@ class NeutronOVSHooksTests(CharmTestCase): def test_config_changed(self): self._call_hook('config-changed') self.assertTrue(self.CONFIGS.write_all.called) + self.configure_ovs.assert_called_with() + + def test_config_changed_dvr(self): + self.determine_dvr_packages.return_value = ['dvr'] + self._call_hook('config-changed') + self.apt_update.assert_called_with() + self.assertTrue(self.CONFIGS.write_all.called) + self.apt_install.assert_has_calls([ + call(['dvr'], fatal=True), + ]) + self.configure_ovs.assert_called_with() + + @patch.object(hooks, 'neutron_plugin_joined') + def test_neutron_plugin_api(self, _plugin_joined): + self.relation_ids.return_value = ['rid'] + self._call_hook('neutron-plugin-api-relation-changed') + self.configure_ovs.assert_called_with() + self.assertTrue(self.CONFIGS.write_all.called) + _plugin_joined.assert_called_with(relation_id='rid') + + def test_neutron_plugin_joined(self): + self.get_shared_secret.return_value = 'secret' + self._call_hook('neutron-plugin-relation-joined') + rel_data = { + 'metadata-shared-secret': 'secret', + } + self.relation_set.assert_called_with( + relation_id=None, + **rel_data + ) def test_amqp_joined(self): self._call_hook('amqp-relation-joined') diff --git a/unit_tests/test_neutron_ovs_utils.py b/unit_tests/test_neutron_ovs_utils.py index 5d0a304b..dd1c5328 100644 --- a/unit_tests/test_neutron_ovs_utils.py +++ b/unit_tests/test_neutron_ovs_utils.py @@ -1,11 +1,12 @@ -from mock import MagicMock, patch +from mock import MagicMock, patch, call from collections import OrderedDict import charmhelpers.contrib.openstack.templating as templating templating.OSConfigRenderer = MagicMock() import neutron_ovs_utils as nutils +import neutron_ovs_context from test_utils import ( CharmTestCase, @@ -15,8 +16,15 @@ import charmhelpers.core.hookenv as hookenv TO_PATCH = [ + 'add_bridge', + 'add_bridge_port', + 'config', 'os_release', 'neutron_plugin_attribute', + 'full_restart', + 'service_restart', + 'service_running', + 'ExternalPortContext', ] head_pkg = 'linux-headers-3.15.0-5-generic' @@ -38,26 +46,39 @@ def _mock_npa(plugin, attr, net_manager=None): return plugins[plugin][attr] +class DummyContext(): + + def __init__(self, return_value): + self.return_value = return_value + + def __call__(self): + return self.return_value + + class TestNeutronOVSUtils(CharmTestCase): def setUp(self): super(TestNeutronOVSUtils, self).setUp(nutils, TO_PATCH) self.neutron_plugin_attribute.side_effect = _mock_npa + self.config.side_effect = self.test_config.get def tearDown(self): # Reset cached cache hookenv.cache = {} + @patch.object(nutils, 'use_dvr') @patch.object(charmhelpers.contrib.openstack.neutron, 'os_release') @patch.object(charmhelpers.contrib.openstack.neutron, 'headers_package') - def test_determine_packages(self, _head_pkgs, _os_rel): + def test_determine_packages(self, _head_pkgs, _os_rel, _use_dvr): + _use_dvr.return_value = False _os_rel.return_value = 'trusty' _head_pkgs.return_value = head_pkg pkg_list = nutils.determine_packages() expect = [['neutron-plugin-openvswitch-agent'], [head_pkg]] self.assertItemsEqual(pkg_list, expect) - def test_register_configs(self): + @patch.object(nutils, 'use_dvr') + def test_register_configs(self, _use_dvr): class _mock_OSConfigRenderer(): def __init__(self, templates_dir=None, openstack_release=None): self.configs = [] @@ -67,6 +88,7 @@ class TestNeutronOVSUtils(CharmTestCase): self.configs.append(config) self.ctxts.append(ctxt) + _use_dvr.return_value = False self.os_release.return_value = 'trusty' templating.OSConfigRenderer.side_effect = _mock_OSConfigRenderer _regconfs = nutils.register_configs() @@ -75,12 +97,28 @@ class TestNeutronOVSUtils(CharmTestCase): '/etc/init/os-charm-phy-nic-mtu.conf'] self.assertItemsEqual(_regconfs.configs, confs) - def test_resource_map(self): + @patch.object(nutils, 'use_dvr') + def test_resource_map(self, _use_dvr): + _use_dvr.return_value = False _map = nutils.resource_map() + svcs = ['neutron-plugin-openvswitch-agent'] confs = [nutils.NEUTRON_CONF] [self.assertIn(q_conf, _map.keys()) for q_conf in confs] + self.assertEqual(_map[nutils.NEUTRON_CONF]['services'], svcs) - def test_restart_map(self): + @patch.object(nutils, 'use_dvr') + def test_resource_map_dvr(self, _use_dvr): + _use_dvr.return_value = True + _map = nutils.resource_map() + svcs = ['neutron-plugin-openvswitch-agent', 'neutron-metadata-agent', + 'neutron-l3-agent'] + confs = [nutils.NEUTRON_CONF] + [self.assertIn(q_conf, _map.keys()) for q_conf in confs] + self.assertEqual(_map[nutils.NEUTRON_CONF]['services'], svcs) + + @patch.object(nutils, 'use_dvr') + def test_restart_map(self, _use_dvr): + _use_dvr.return_value = False _restart_map = nutils.restart_map() ML2CONF = "/etc/neutron/plugins/ml2/ml2_conf.ini" expect = OrderedDict([ @@ -92,3 +130,79 @@ class TestNeutronOVSUtils(CharmTestCase): for item in _restart_map: self.assertTrue(item in _restart_map) self.assertTrue(expect[item] == _restart_map[item]) + + @patch.object(nutils, 'use_dvr') + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_ovs_data_port(self, mock_config, _use_dvr): + _use_dvr.return_value = False + mock_config.side_effect = self.test_config.get + self.config.side_effect = self.test_config.get + self.ExternalPortContext.return_value = \ + DummyContext(return_value=None) + # Test back-compatibility i.e. port but no bridge (so br-data is + # assumed) + self.test_config.set('data-port', 'eth0') + nutils.configure_ovs() + self.add_bridge.assert_has_calls([ + call('br-int'), + call('br-ex'), + call('br-data') + ]) + self.assertTrue(self.add_bridge_port.called) + + # Now test with bridge:port format + self.test_config.set('data-port', 'br-foo:eth0') + self.add_bridge.reset_mock() + self.add_bridge_port.reset_mock() + nutils.configure_ovs() + self.add_bridge.assert_has_calls([ + call('br-int'), + call('br-ex'), + call('br-data') + ]) + # Not called since we have a bogus bridge in data-ports + self.assertFalse(self.add_bridge_port.called) + + @patch.object(nutils, 'use_dvr') + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_starts_service_if_required(self, mock_config, + _use_dvr): + _use_dvr.return_value = False + mock_config.side_effect = self.test_config.get + self.config.return_value = 'ovs' + self.service_running.return_value = False + nutils.configure_ovs() + self.assertTrue(self.full_restart.called) + + @patch.object(nutils, 'use_dvr') + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_doesnt_restart_service(self, mock_config, _use_dvr): + _use_dvr.return_value = False + mock_config.side_effect = self.test_config.get + self.config.side_effect = self.test_config.get + self.service_running.return_value = True + nutils.configure_ovs() + self.assertFalse(self.full_restart.called) + + @patch.object(nutils, 'use_dvr') + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_ovs_ext_port(self, mock_config, _use_dvr): + _use_dvr.return_value = True + mock_config.side_effect = self.test_config.get + self.config.side_effect = self.test_config.get + self.test_config.set('ext-port', 'eth0') + self.ExternalPortContext.return_value = \ + DummyContext(return_value={'ext_port': 'eth0'}) + nutils.configure_ovs() + self.add_bridge.assert_has_calls([ + call('br-int'), + call('br-ex'), + call('br-data') + ]) + self.add_bridge_port.assert_called_with('br-ex', 'eth0') + + @patch.object(neutron_ovs_context, 'DVRSharedSecretContext') + def test_get_shared_secret(self, _dvr_secret_ctxt): + _dvr_secret_ctxt.return_value = \ + DummyContext(return_value={'shared_secret': 'supersecret'}) + self.assertEqual(nutils.get_shared_secret(), 'supersecret')