diff --git a/.bzrignore b/.bzrignore index a2c7a097..421e2bda 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,2 +1,3 @@ bin .coverage +tags diff --git a/Makefile b/Makefile index db4e2c25..6ca58a67 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PYTHON := /usr/bin/env python lint: - @flake8 --exclude hooks/charmhelpers hooks unit_tests tests + @flake8 --exclude hooks/charmhelpers actions hooks unit_tests tests @charm proof unit_test: @@ -15,7 +15,7 @@ bin/charm_helpers_sync.py: > bin/charm_helpers_sync.py sync: bin/charm_helpers_sync.py - @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-sync.yaml + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml test: @@ -25,7 +25,8 @@ test: # https://bugs.launchpad.net/amulet/+bug/1320357 @juju test -v -p AMULET_HTTP_PROXY --timeout 900 \ 00-setup 14-basic-precise-icehouse 15-basic-trusty-icehouse \ - 16-basic-trusty-juno + 16-basic-trusty-icehouse-git 17-basic-trusty-juno \ + 18-basic-trusty-juno-git publish: lint unit_test bzr push lp:charms/neutron-openvswitch diff --git a/README.md b/README.md index 6318fa44..05d08869 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,90 @@ This charm has a configuration option to allow users to disable any per-instance ... These compute nodes could then be accessed by cloud users via use of host aggregates with specific flavors to target instances to hypervisors with no per-instance security. + +# Deploying from source + +The minimum openstack-origin-git config required to deploy from source is: + + openstack-origin-git: + "repositories: + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements', + branch: stable/juno} + - {name: neutron, + repository: 'git://git.openstack.org/openstack/neutron', + branch: stable/juno}" + +Note that there are only two 'name' values the charm knows about: 'requirements' +and 'neutron'. These repositories must correspond to these 'name' values. +Additionally, the requirements repository must be specified first and the +neutron repository must be specified last. All other repostories are installed +in the order in which they are specified. + +The following is a full list of current tip repos (may not be up-to-date): + + openstack-origin-git: + "repositories: + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements', + branch: master} + - {name: oslo-concurrency, + repository: 'git://git.openstack.org/openstack/oslo.concurrency', + branch: master} + - {name: oslo-config, + repository: 'git://git.openstack.org/openstack/oslo.config', + branch: master} + - {name: oslo-context, + repository: 'git://git.openstack.org/openstack/oslo.context.git', + branch: master} + - {name: oslo-db, + repository: 'git://git.openstack.org/openstack/oslo.db', + branch: master} + - {name: oslo-i18n, + repository: 'git://git.openstack.org/openstack/oslo.i18n', + branch: master} + - {name: oslo-messaging, + repository: 'git://git.openstack.org/openstack/oslo.messaging.git', + branch: master} + - {name: oslo-middleware, + repository': 'git://git.openstack.org/openstack/oslo.middleware.git', + branch: master} + - {name: oslo-rootwrap', + repository: 'git://git.openstack.org/openstack/oslo.rootwrap.git', + branch: master} + - {name: oslo-serialization, + repository: 'git://git.openstack.org/openstack/oslo.serialization', + branch: master} + - {name: oslo-utils, + repository: 'git://git.openstack.org/openstack/oslo.utils', + branch: master} + - {name: pbr, + repository: 'git://git.openstack.org/openstack-dev/pbr', + branch: master} + - {name: stevedore, + repository: 'git://git.openstack.org/openstack/stevedore.git', + branch: 'master'} + - {name: python-keystoneclient, + repository: 'git://git.openstack.org/openstack/python-keystoneclient', + branch: master} + - {name: python-neutronclient, + repository: 'git://git.openstack.org/openstack/python-neutronclient.git', + branch: master} + - {name: python-novaclient, + repository': 'git://git.openstack.org/openstack/python-novaclient.git', + branch: master} + - {name: keystonemiddleware, + repository: 'git://git.openstack.org/openstack/keystonemiddleware', + branch: master} + - {name: neutron-fwaas, + repository': 'git://git.openstack.org/openstack/neutron-fwaas.git', + branch: master} + - {name: neutron-lbaas, + repository: 'git://git.openstack.org/openstack/neutron-lbaas.git', + branch: master} + - {name: neutron-vpnaas, + repository: 'git://git.openstack.org/openstack/neutron-vpnaas.git', + branch: master} + - {name: neutron, + repository: 'git://git.openstack.org/openstack/neutron', + branch: master}" diff --git a/actions.yaml b/actions.yaml new file mode 100644 index 00000000..02fbf59a --- /dev/null +++ b/actions.yaml @@ -0,0 +1,2 @@ +git-reinstall: + description: Reinstall neutron-openvswitch from the openstack-origin-git repositories. diff --git a/actions/git-reinstall b/actions/git-reinstall new file mode 120000 index 00000000..ff684984 --- /dev/null +++ b/actions/git-reinstall @@ -0,0 +1 @@ +git_reinstall.py \ No newline at end of file diff --git a/actions/git_reinstall.py b/actions/git_reinstall.py new file mode 100755 index 00000000..849e6634 --- /dev/null +++ b/actions/git_reinstall.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +import sys +import traceback + +sys.path.append('hooks/') + +from charmhelpers.contrib.openstack.utils import ( + git_install_requested, +) + +from charmhelpers.core.hookenv import ( + action_set, + action_fail, + config, +) + +from neutron_ovs_utils import ( + git_install, +) + + +def git_reinstall(): + """Reinstall from source and restart services. + + If the openstack-origin-git config option was used to install openstack + from source git repositories, then this action can be used to reinstall + from updated git repositories, followed by a restart of services.""" + if not git_install_requested(): + action_fail('openstack-origin-git is not configured') + return + + try: + git_install(config('openstack-origin-git')) + except: + action_set({'traceback': traceback.format_exc()}) + action_fail('git-reinstall resulted in an unexpected error') + + +if __name__ == '__main__': + git_reinstall() diff --git a/charm-helpers-sync.yaml b/charm-helpers-hooks.yaml similarity index 100% rename from charm-helpers-sync.yaml rename to charm-helpers-hooks.yaml diff --git a/config.yaml b/config.yaml index 41699d49..9f96a737 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,21 @@ options: + openstack-origin-git: + default: + type: string + description: | + Specifies a YAML-formatted dictionary listing the git + repositories and branches from which to install OpenStack and + its dependencies. + + When openstack-origin-git is specified, openstack-specific + packages will be installed from source rather than from the + the nova-compute charm's openstack-origin repository. + + Note that the installed config files will be determined based on + the OpenStack release of the nova-compute charm's openstack-origin + option. + + For more details see README.md. rabbit-user: default: neutron type: string @@ -60,3 +77,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/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, ('precise', 'cloud:precise-havana'): self.precise_havana, ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, - ('trusty', None): self.trusty_icehouse} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 90ac6d69..dd51bfbb 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -47,6 +47,7 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.core.sysctl import create as sysctl_create +from charmhelpers.core.strutils import bool_from_string from charmhelpers.core.host import ( list_nics, @@ -67,6 +68,7 @@ from charmhelpers.contrib.hahelpers.apache import ( ) from charmhelpers.contrib.openstack.neutron import ( neutron_plugin_attribute, + parse_data_port_mappings, ) from charmhelpers.contrib.openstack.ip import ( resolve_address, @@ -82,7 +84,6 @@ from charmhelpers.contrib.network.ip import ( is_bridge_member, ) from charmhelpers.contrib.openstack.utils import get_host_ip - CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' ADDRESS_TYPES = ['admin', 'internal', 'public'] @@ -319,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: @@ -340,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') @@ -1162,3 +1164,145 @@ class SysctlContext(OSContextGenerator): sysctl_create(sysctl_dict, '/etc/sysctl.d/50-{0}.conf'.format(charm_name())) return {'sysctl': sysctl_dict} + + +class NeutronAPIContext(OSContextGenerator): + ''' + Inspects current neutron-plugin-api relation for neutron settings. Return + defaults if it is not present. + ''' + interfaces = ['neutron-plugin-api'] + + def __call__(self): + self.neutron_defaults = { + 'l2_population': { + 'rel_key': 'l2-population', + 'default': False, + }, + 'overlay_network_type': { + 'rel_key': 'overlay-network-type', + 'default': 'gre', + }, + 'neutron_security_groups': { + 'rel_key': 'neutron-security-groups', + 'default': False, + }, + 'network_device_mtu': { + 'rel_key': 'network-device-mtu', + 'default': None, + }, + 'enable_dvr': { + 'rel_key': 'enable-dvr', + 'default': False, + }, + 'enable_l3ha': { + 'rel_key': 'enable-l3ha', + 'default': False, + }, + } + ctxt = self.get_neutron_options({}) + for rid in relation_ids('neutron-plugin-api'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + if 'l2-population' in rdata: + ctxt.update(self.get_neutron_options(rdata)) + + return ctxt + + def get_neutron_options(self, rdata): + settings = {} + for nkey in self.neutron_defaults.keys(): + defv = self.neutron_defaults[nkey]['default'] + rkey = self.neutron_defaults[nkey]['rel_key'] + if rkey in rdata.keys(): + if type(defv) is bool: + settings[nkey] = bool_from_string(rdata[rkey]) + else: + settings[nkey] = rdata[rkey] + else: + settings[nkey] = defv + return settings + + +class ExternalPortContext(NeutronPortContext): + + def __call__(self): + ctxt = {} + ports = config('ext-port') + if ports: + ports = [p.strip() for p in ports.split()] + ports = self.resolve_ports(ports) + if ports: + ctxt = {"ext_port": ports[0]} + napi_settings = NeutronAPIContext()() + mtu = napi_settings.get('network_device_mtu') + if mtu: + ctxt['ext_port_mtu'] = mtu + + return ctxt + + +class DataPortContext(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 + six.iteritems(portmap) if port in normalized.keys()} + + return None + + +class PhyNICMTUContext(DataPortContext): + + def __call__(self): + ctxt = {} + mappings = super(PhyNICMTUContext, self).__call__() + if mappings and mappings.values(): + ports = mappings.values() + napi_settings = NeutronAPIContext()() + mtu = napi_settings.get('network_device_mtu') + if mtu: + ctxt["devs"] = '\\n'.join(ports) + ctxt['mtu'] = mtu + + return ctxt + + +class NetworkServiceContext(OSContextGenerator): + + def __init__(self, rel_name='quantum-network-service'): + self.rel_name = rel_name + self.interfaces = [rel_name] + + def __call__(self): + for rid in relation_ids(self.rel_name): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + ctxt = { + 'keystone_host': rdata.get('keystone_host'), + 'service_port': rdata.get('service_port'), + 'auth_port': rdata.get('auth_port'), + 'service_tenant': rdata.get('service_tenant'), + 'service_username': rdata.get('service_username'), + 'service_password': rdata.get('service_password'), + 'quantum_host': rdata.get('quantum_host'), + 'quantum_port': rdata.get('quantum_port'), + 'quantum_url': rdata.get('quantum_url'), + 'region': rdata.get('region'), + 'service_protocol': + rdata.get('service_protocol') or 'http', + 'auth_protocol': + rdata.get('auth_protocol') or 'http', + } + if context_complete(ctxt): + return ctxt + return {} diff --git a/hooks/charmhelpers/contrib/openstack/templates/git.upstart b/hooks/charmhelpers/contrib/openstack/templates/git.upstart new file mode 100644 index 00000000..da94ad12 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/git.upstart @@ -0,0 +1,13 @@ +description "{{ service_description }}" +author "Juju {{ service_name }} Charm " + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn + +exec start-stop-daemon --start --chuid {{ user_name }} \ + --chdir {{ start_dir }} --name {{ process_name }} \ + --exec {{ executable_name }} -- \ + --config-file={{ config_file }} \ + --log-file={{ log_file }} diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken new file mode 100644 index 00000000..2a37edd5 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken @@ -0,0 +1,9 @@ +{% if auth_host -%} +[keystone_authtoken] +identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }} +auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +signing_dir = {{ signing_dir }} +{% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo new file mode 100644 index 00000000..b444c9c9 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo @@ -0,0 +1,22 @@ +{% if rabbitmq_host or rabbitmq_hosts -%} +[oslo_messaging_rabbit] +rabbit_userid = {{ rabbitmq_user }} +rabbit_virtual_host = {{ rabbitmq_virtual_host }} +rabbit_password = {{ rabbitmq_password }} +{% if rabbitmq_hosts -%} +rabbit_hosts = {{ rabbitmq_hosts }} +{% if rabbitmq_ha_queues -%} +rabbit_ha_queues = True +rabbit_durable_queues = False +{% endif -%} +{% else -%} +rabbit_host = {{ rabbitmq_host }} +{% endif -%} +{% if rabbit_ssl_port -%} +rabbit_use_ssl = True +rabbit_port = {{ rabbit_ssl_port }} +{% if rabbit_ssl_ca -%} +kombu_ssl_ca_certs = {{ rabbit_ssl_ca }} +{% endif -%} +{% endif -%} +{% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/templates/zeromq b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq similarity index 66% rename from hooks/charmhelpers/contrib/openstack/templates/zeromq rename to hooks/charmhelpers/contrib/openstack/templates/section-zeromq index 0695eef1..95f1a76c 100644 --- a/hooks/charmhelpers/contrib/openstack/templates/zeromq +++ b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq @@ -3,12 +3,12 @@ rpc_backend = zmq rpc_zmq_host = {{ zmq_host }} {% if zmq_redis_address -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis +rpc_zmq_matchmaker = redis matchmaker_heartbeat_freq = 15 matchmaker_heartbeat_ttl = 30 [matchmaker_redis] host = {{ zmq_redis_address }} {% else -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing +rpc_zmq_matchmaker = ring {% endif -%} {% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 4f110c63..5a12c9d6 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -30,6 +30,10 @@ import yaml from charmhelpers.contrib.network import ip +from charmhelpers.core import ( + unitdata, +) + from charmhelpers.core.hookenv import ( config, log as juju_log, @@ -330,6 +334,21 @@ def configure_installation_source(rel): error_out("Invalid openstack-release specified: %s" % rel) +def config_value_changed(option): + """ + Determine if config value changed since last call to this function. + """ + hook_data = unitdata.HookData() + with hook_data(): + db = unitdata.kv() + current = config(option) + saved = db.get(option) + db.set(option, current) + if saved is None: + return False + return current != saved + + def save_script_rc(script_path="scripts/scriptrc", **env_vars): """ Write an rc file in the charm-delivered directory containing @@ -469,82 +488,103 @@ def os_requires_version(ostack_release, pkg): def git_install_requested(): - """Returns true if openstack-origin-git is specified.""" - return config('openstack-origin-git') != "None" + """ + Returns true if openstack-origin-git is specified. + """ + return config('openstack-origin-git') is not None requirements_dir = None -def git_clone_and_install(file_name, core_project): - """Clone/install all OpenStack repos specified in yaml config file.""" - global requirements_dir +def git_clone_and_install(projects_yaml, core_project): + """ + Clone/install all specified OpenStack repositories. - if file_name == "None": + The expected format of projects_yaml is: + repositories: + - {name: keystone, + repository: 'git://git.openstack.org/openstack/keystone.git', + branch: 'stable/icehouse'} + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements.git', + branch: 'stable/icehouse'} + directory: /mnt/openstack-git + http_proxy: http://squid.internal:3128 + https_proxy: https://squid.internal:3128 + + The directory, http_proxy, and https_proxy keys are optional. + """ + global requirements_dir + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: return - yaml_file = os.path.join(charm_dir(), file_name) + projects = yaml.load(projects_yaml) + _git_validate_projects_yaml(projects, core_project) - # clone/install the requirements project first - installed = _git_clone_and_install_subset(yaml_file, - whitelist=['requirements']) - if 'requirements' not in installed: - error_out('requirements git repository must be specified') + if 'http_proxy' in projects.keys(): + os.environ['http_proxy'] = projects['http_proxy'] - # clone/install all other projects except requirements and the core project - blacklist = ['requirements', core_project] - _git_clone_and_install_subset(yaml_file, blacklist=blacklist, - update_requirements=True) + if 'https_proxy' in projects.keys(): + os.environ['https_proxy'] = projects['https_proxy'] - # clone/install the core project - whitelist = [core_project] - installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist, - update_requirements=True) - if core_project not in installed: - error_out('{} git repository must be specified'.format(core_project)) + if 'directory' in projects.keys(): + parent_dir = projects['directory'] + + for p in projects['repositories']: + repo = p['repository'] + branch = p['branch'] + if p['name'] == 'requirements': + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=False) + requirements_dir = repo_dir + else: + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=True) -def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[], - update_requirements=False): - """Clone/install subset of OpenStack repos specified in yaml config file.""" - global requirements_dir - installed = [] +def _git_validate_projects_yaml(projects, core_project): + """ + Validate the projects yaml. + """ + _git_ensure_key_exists('repositories', projects) - with open(yaml_file, 'r') as fd: - projects = yaml.load(fd) - for proj, val in projects.items(): - # The project subset is chosen based on the following 3 rules: - # 1) If project is in blacklist, we don't clone/install it, period. - # 2) If whitelist is empty, we clone/install everything else. - # 3) If whitelist is not empty, we clone/install everything in the - # whitelist. - if proj in blacklist: - continue - if whitelist and proj not in whitelist: - continue - repo = val['repository'] - branch = val['branch'] - repo_dir = _git_clone_and_install_single(repo, branch, - update_requirements) - if proj == 'requirements': - requirements_dir = repo_dir - installed.append(proj) - return installed + for project in projects['repositories']: + _git_ensure_key_exists('name', project.keys()) + _git_ensure_key_exists('repository', project.keys()) + _git_ensure_key_exists('branch', project.keys()) + + if projects['repositories'][0]['name'] != 'requirements': + error_out('{} git repo must be specified first'.format('requirements')) + + if projects['repositories'][-1]['name'] != core_project: + error_out('{} git repo must be specified last'.format(core_project)) -def _git_clone_and_install_single(repo, branch, update_requirements=False): - """Clone and install a single git repository.""" - dest_parent_dir = "/mnt/openstack-git/" - dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo)) +def _git_ensure_key_exists(key, keys): + """ + Ensure that key exists in keys. + """ + if key not in keys: + error_out('openstack-origin-git key \'{}\' is missing'.format(key)) - if not os.path.exists(dest_parent_dir): - juju_log('Host dir not mounted at {}. ' - 'Creating directory there instead.'.format(dest_parent_dir)) - os.mkdir(dest_parent_dir) + +def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements): + """ + Clone and install a single git repository. + """ + dest_dir = os.path.join(parent_dir, os.path.basename(repo)) + + if not os.path.exists(parent_dir): + juju_log('Directory already exists at {}. ' + 'No need to create directory.'.format(parent_dir)) + os.mkdir(parent_dir) if not os.path.exists(dest_dir): juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) - repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch) + repo_dir = install_remote(repo, dest=parent_dir, branch=branch) else: repo_dir = dest_dir @@ -561,16 +601,39 @@ def _git_clone_and_install_single(repo, branch, update_requirements=False): def _git_update_requirements(package_dir, reqs_dir): - """Update from global requirements. + """ + Update from global requirements. - Update an OpenStack git directory's requirements.txt and - test-requirements.txt from global-requirements.txt.""" + Update an OpenStack git directory's requirements.txt and + test-requirements.txt from global-requirements.txt. + """ orig_dir = os.getcwd() os.chdir(reqs_dir) - cmd = "python update.py {}".format(package_dir) + cmd = ['python', 'update.py', package_dir] try: - subprocess.check_call(cmd.split(' ')) + subprocess.check_call(cmd) except subprocess.CalledProcessError: package = os.path.basename(package_dir) error_out("Error updating {} from global-requirements.txt".format(package)) os.chdir(orig_dir) + + +def git_src_dir(projects_yaml, project): + """ + Return the directory where the specified project's source is located. + """ + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: + return + + projects = yaml.load(projects_yaml) + + if 'directory' in projects.keys(): + parent_dir = projects['directory'] + + for p in projects['repositories']: + if p['name'] == project: + return os.path.join(parent_dir, os.path.basename(p['repository'])) + + return None diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py index 3000134a..406a35c5 100644 --- a/hooks/charmhelpers/core/unitdata.py +++ b/hooks/charmhelpers/core/unitdata.py @@ -443,7 +443,7 @@ class HookData(object): data = hookenv.execution_environment() self.conf = conf_delta = self.kv.delta(data['conf'], 'config') self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') - self.kv.set('env', data['env']) + self.kv.set('env', dict(data['env'])) self.kv.set('unit', data['unit']) self.kv.set('relid', data.get('relid')) return conf_delta, rels_delta diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py index 4c6febf5..4ca582f9 100644 --- a/hooks/neutron_ovs_context.py +++ b/hooks/neutron_ovs_context.py @@ -1,80 +1,23 @@ +import os +import uuid from charmhelpers.core.hookenv import ( + config, + relation_get, relation_ids, related_units, - relation_get, - config, unit_get, ) -from charmhelpers.core.strutils import bool_from_string +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, +) 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' - - -def _neutron_api_settings(): - ''' - Inspects current neutron-plugin relation - ''' - neutron_settings = { - 'neutron_security_groups': False, - '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' 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 - - 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): @@ -90,27 +33,11 @@ class OVSPluginContext(context.NeutronContext): @property def neutron_security_groups(self): - neutron_api_settings = _neutron_api_settings() + if config('disable-security-groups'): + return False + 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 @@ -119,15 +46,14 @@ 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'), get_host_ip(unit_get('private-address'))) - neutron_api_settings = _neutron_api_settings() + 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 @@ -157,18 +83,60 @@ class OVSPluginContext(context.NeutronContext): return ovs_ctxt -class PhyNICMTUContext(DataPortContext): - """Context used to apply settings to neutron data-port devices""" +class L3AgentContext(OSContextGenerator): def __call__(self): + neutron_api_settings = NeutronAPIContext()() 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 - + 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..887e3801 100755 --- a/hooks/neutron_ovs_hooks.py +++ b/hooks/neutron_ovs_hooks.py @@ -2,12 +2,18 @@ import sys +from charmhelpers.contrib.openstack.utils import ( + config_value_changed, + git_install_requested, +) + from charmhelpers.core.hookenv import ( Hooks, UnregisteredHookError, config, log, relation_set, + relation_ids, ) from charmhelpers.core.host import ( @@ -15,13 +21,24 @@ from charmhelpers.core.host import ( ) from charmhelpers.fetch import ( - apt_install, apt_update + apt_install, apt_update, apt_purge +) + +from charmhelpers.contrib.openstack.utils import ( + os_requires_version, ) from neutron_ovs_utils import ( + DVR_PACKAGES, + configure_ovs, determine_packages, + git_install, + get_topics, + determine_dvr_packages, + get_shared_secret, register_configs, restart_map, + use_dvr, ) hooks = Hooks() @@ -35,13 +52,49 @@ def install(): for pkg in pkgs: apt_install(pkg, fatal=True) + git_install(config('openstack-origin-git')) + @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) + + if git_install_requested(): + if config_value_changed('openstack-origin-git'): + git_install(config('openstack-origin-git')) + + configure_ovs() CONFIGS.write_all() + for rid in relation_ids('zeromq-configuration'): + zeromq_configuration_relation_joined(rid) + + +@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') @@ -61,6 +114,20 @@ def amqp_changed(): CONFIGS.write_all() +@hooks.hook('zeromq-configuration-relation-joined') +@os_requires_version('kilo', 'neutron-common') +def zeromq_configuration_relation_joined(relid=None): + relation_set(relation_id=relid, + topics=" ".join(get_topics()), + users="neutron") + + +@hooks.hook('zeromq-configuration-relation-changed') +@restart_on_change(restart_map(), stopstart=True) +def zeromq_configuration_relation_changed(): + CONFIGS.write_all() + + def main(): try: hooks.execute(sys.argv) diff --git a/hooks/neutron_ovs_utils.py b/hooks/neutron_ovs_utils.py index 5cc9871c..fb9d5dd6 100644 --- a/hooks/neutron_ovs_utils.py +++ b/hooks/neutron_ovs_utils.py @@ -1,18 +1,76 @@ +import os +import shutil + from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute from copy import deepcopy from charmhelpers.contrib.openstack import context, templating +from charmhelpers.contrib.openstack.utils import ( + git_install_requested, + git_clone_and_install, + git_src_dir, +) from collections import OrderedDict 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 ( + adduser, + add_group, + add_user_to_group, + mkdir, + service_restart, + service_running, + write_file, +) + +from charmhelpers.core.templating import render + +BASE_GIT_PACKAGES = [ + 'libxml2-dev', + 'libxslt1-dev', + 'openvswitch-switch', + 'python-dev', + 'python-pip', + 'python-setuptools', + 'zlib1g-dev', +] + +# ubuntu packages that should not be installed when deploying from git +GIT_PACKAGE_BLACKLIST = [ + 'neutron-l3-agent', + 'neutron-metadata-agent', + 'neutron-server', + 'neutron-plugin-openvswitch', + 'neutron-plugin-openvswitch-agent', +] 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/' @@ -20,7 +78,9 @@ BASE_RESOURCE_MAP = OrderedDict([ (NEUTRON_CONF, { 'services': ['neutron-plugin-openvswitch-agent'], 'contexts': [neutron_ovs_context.OVSPluginContext(), - context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR)], + context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR), + context.ZeroMQContext(), + context.NotificationDriverContext()], }), (ML2_CONF, { 'services': ['neutron-plugin-openvswitch-agent'], @@ -28,13 +88,53 @@ BASE_RESOURCE_MAP = OrderedDict([ }), (PHY_NIC_MTU_CONF, { 'services': ['os-charm-phy-nic-mtu'], - 'contexts': [neutron_ovs_context.PhyNICMTUContext()], + '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 not git_install_requested(): + 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()) + + if git_install_requested(): + pkgs.extend(BASE_GIT_PACKAGES) + # don't include packages that will be installed from git + for p in GIT_PACKAGE_BLACKLIST: + if p in pkgs: + pkgs.remove(p) + + return pkgs def register_configs(release=None): @@ -52,6 +152,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 +165,134 @@ def restart_map(): state. ''' return {k: v['services'] for k, v in resource_map().iteritems()} + + +def get_topics(): + topics = [] + topics.append('q-agent-notifier-port-update') + topics.append('q-agent-notifier-network-delete') + topics.append('q-agent-notifier-tunnel-update') + topics.append('q-agent-notifier-security_group-update') + topics.append('q-agent-notifier-dvr-update') + if context.NeutronAPIContext()()['l2_population']: + topics.append('q-agent-notifier-l2population-update') + return topics + + +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'] + + +def git_install(projects_yaml): + """Perform setup, and install git repos specified in yaml parameter.""" + if git_install_requested(): + git_pre_install() + git_clone_and_install(projects_yaml, core_project='neutron') + git_post_install(projects_yaml) + + +def git_pre_install(): + """Perform pre-install setup.""" + dirs = [ + '/etc/neutron', + '/etc/neutron/rootwrap.d', + '/var/lib/neutron', + '/var/lib/neutron/lock', + '/var/log/neutron', + ] + + logs = [ + '/var/log/neutron/server.log', + ] + + adduser('neutron', shell='/bin/bash', system_user=True) + add_group('neutron', system_group=True) + add_user_to_group('neutron', 'neutron') + + for d in dirs: + mkdir(d, owner='neutron', group='neutron', perms=0700, force=False) + + for l in logs: + write_file(l, '', owner='neutron', group='neutron', perms=0600) + + +def git_post_install(projects_yaml): + """Perform post-install setup.""" + src_etc = os.path.join(git_src_dir(projects_yaml, 'neutron'), 'etc') + configs = { + 'debug-filters': { + 'src': os.path.join(src_etc, 'neutron/rootwrap.d/debug.filters'), + 'dest': '/etc/neutron/rootwrap.d/debug.filters', + }, + 'policy': { + 'src': os.path.join(src_etc, 'policy.json'), + 'dest': '/etc/neutron/policy.json', + }, + 'rootwrap': { + 'src': os.path.join(src_etc, 'rootwrap.conf'), + 'dest': '/etc/neutron/rootwrap.conf', + }, + } + + for conf, files in configs.iteritems(): + shutil.copyfile(files['src'], files['dest']) + + render('neutron_sudoers', '/etc/sudoers.d/neutron_sudoers', {}, + perms=0o440) + + neutron_ovs_agent_context = { + 'service_description': 'Neutron OpenvSwitch Plugin Agent', + 'charm_name': 'neutron-openvswitch', + 'process_name': 'neutron-openvswitch-agent', + 'cleanup_process_name': 'neutron-ovs-cleanup', + 'plugin_config': '/etc/neutron/plugins/ml2/ml2_conf.ini', + 'log_file': '/var/log/neutron/openvswitch-agent.log', + } + + neutron_ovs_cleanup_context = { + 'service_description': 'Neutron OpenvSwitch Cleanup', + 'charm_name': 'neutron-openvswitch', + 'process_name': 'neutron-ovs-cleanup', + 'log_file': '/var/log/neutron/ovs-cleanup.log', + } + + # NOTE(coreycb): Needs systemd support + render('upstart/neutron-plugin-openvswitch-agent.upstart', + '/etc/init/neutron-plugin-openvswitch-agent.conf', + neutron_ovs_agent_context, perms=0o644) + render('upstart/neutron-ovs-cleanup.upstart', + '/etc/init/neutron-ovs-cleanup.conf', + neutron_ovs_cleanup_context, perms=0o644) + + service_restart('neutron-plugin-openvswitch-agent') diff --git a/hooks/zeromq-configuration-relation-changed b/hooks/zeromq-configuration-relation-changed new file mode 120000 index 00000000..55aa8e52 --- /dev/null +++ b/hooks/zeromq-configuration-relation-changed @@ -0,0 +1 @@ +neutron_ovs_hooks.py \ No newline at end of file diff --git a/hooks/zeromq-configuration-relation-joined b/hooks/zeromq-configuration-relation-joined new file mode 120000 index 00000000..55aa8e52 --- /dev/null +++ b/hooks/zeromq-configuration-relation-joined @@ -0,0 +1 @@ +neutron_ovs_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 0e840258..dfbefd7b 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -28,3 +28,7 @@ requires: scope: container neutron-plugin-api: interface: neutron-plugin-api + zeromq-configuration: + interface: zeromq-configuration + scope: container + 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/icehouse/neutron.conf b/templates/icehouse/neutron.conf index c7af465a..25004db5 100644 --- a/templates/icehouse/neutron.conf +++ b/templates/icehouse/neutron.conf @@ -12,15 +12,18 @@ state_path = /var/lib/neutron lock_path = $state_path/lock bind_host = 0.0.0.0 bind_port = 9696 +{% if network_device_mtu -%} network_device_mtu = {{ network_device_mtu }} - +{% endif -%} {% if core_plugin -%} core_plugin = {{ core_plugin }} {% endif -%} api_paste_config = /etc/neutron/api-paste.ini auth_strategy = keystone +{% if notifications == 'True' -%} notification_driver = neutron.openstack.common.notifier.rpc_notifier +{% endif -%} default_notification_level = INFO notification_topics = notifications @@ -35,4 +38,3 @@ root_helper = sudo neutron-rootwrap /etc/neutron/rootwrap.conf [keystone_authtoken] signing_dir = /var/lib/neutron/keystone-signing - 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/templates/kilo/neutron.conf b/templates/kilo/neutron.conf new file mode 100644 index 00000000..7d5748de --- /dev/null +++ b/templates/kilo/neutron.conf @@ -0,0 +1,42 @@ +# icehouse +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +# Config managed by neutron-openvswitch charm +############################################################################### +[DEFAULT] +verbose = {{ verbose }} +debug = {{ debug }} +use_syslog = {{ use_syslog }} +state_path = /var/lib/neutron +bind_host = 0.0.0.0 +bind_port = 9696 +{% if network_device_mtu -%} +network_device_mtu = {{ network_device_mtu }} +{% endif -%} +{% if core_plugin -%} +core_plugin = {{ core_plugin }} +{% endif -%} + +api_paste_config = /etc/neutron/api-paste.ini +auth_strategy = keystone +notification_driver = neutron.openstack.common.notifier.rpc_notifier +default_notification_level = INFO +notification_topics = notifications + +{% include "section-zeromq" %} + +{% include "section-rabbitmq-oslo" %} + +[QUOTAS] + +[DEFAULT_SERVICETYPE] + +[AGENT] +root_helper = sudo neutron-rootwrap /etc/neutron/rootwrap.conf + +[keystone_authtoken] +signing_dir = /var/lib/neutron/keystone-signing + +[oslo_concurrency] +lock_path = $state_path/lock diff --git a/templates/neutron_sudoers b/templates/neutron_sudoers new file mode 100644 index 00000000..d6fec647 --- /dev/null +++ b/templates/neutron_sudoers @@ -0,0 +1,4 @@ +Defaults:neutron !requiretty + +neutron ALL = (root) NOPASSWD: /usr/local/bin/neutron-rootwrap /etc/neutron/rootwrap.conf * + diff --git a/templates/upstart/neutron-ovs-cleanup.upstart b/templates/upstart/neutron-ovs-cleanup.upstart new file mode 100644 index 00000000..cbbd0db2 --- /dev/null +++ b/templates/upstart/neutron-ovs-cleanup.upstart @@ -0,0 +1,17 @@ +description "{{ service_description }}" +author "Juju {{ charm_name }} Charm " + +start on started openvswitch-switch +stop on runlevel [!2345] + +pre-start script + mkdir -p /var/run/neutron + chown neutron:root /var/run/neutron +end script + +pre-start script + [ ! -x /usr/bin/{{ process_name }} ] && exit 0 + start-stop-daemon --start --chuid neutron --exec /usr/local/bin/{{ process_name }} -- \ + --log-file /var/log/neutron/{{ log_file }} \ + --config-file /etc/neutron/neutron.conf --verbose +end script diff --git a/templates/upstart/neutron-plugin-openvswitch-agent.upstart b/templates/upstart/neutron-plugin-openvswitch-agent.upstart new file mode 100644 index 00000000..17a82edb --- /dev/null +++ b/templates/upstart/neutron-plugin-openvswitch-agent.upstart @@ -0,0 +1,18 @@ +description "{{ service_description }}" +author "Juju {{ charm_name }} Charm " + +start on runlevel [2345] and started {{ cleanup_process_name}} +stop on runlevel [!2345] + +respawn + +chdir /var/run + +pre-start script + mkdir -p /var/run/neutron + chown neutron:root /var/run/neutron +end script + +exec start-stop-daemon --start --chuid neutron --exec /usr/local/bin/{{ process_name }} -- \ + --config-file=/etc/neutron/neutron.conf --config-file={{ plugin_config }} \ + --log-file={{ log_file }} diff --git a/tests/16-basic-trusty-icehouse-git b/tests/16-basic-trusty-icehouse-git new file mode 100755 index 00000000..de2fec89 --- /dev/null +++ b/tests/16-basic-trusty-icehouse-git @@ -0,0 +1,9 @@ +#!/usr/bin/python + +"""Amulet tests on a basic neutron-openvswitch git deployment on trusty-icehouse.""" + +from basic_deployment import NeutronOVSBasicDeployment + +if __name__ == '__main__': + deployment = NeutronOVSBasicDeployment(series='trusty', git=True) + deployment.run_tests() diff --git a/tests/16-basic-trusty-juno b/tests/17-basic-trusty-juno similarity index 100% rename from tests/16-basic-trusty-juno rename to tests/17-basic-trusty-juno diff --git a/tests/18-basic-trusty-juno-git b/tests/18-basic-trusty-juno-git new file mode 100755 index 00000000..618c9aa2 --- /dev/null +++ b/tests/18-basic-trusty-juno-git @@ -0,0 +1,12 @@ +#!/usr/bin/python + +"""Amulet tests on a basic neutron-openvswitch git deployment on trusty-juno.""" + +from basic_deployment import NeutronOVSBasicDeployment + +if __name__ == '__main__': + deployment = NeutronOVSBasicDeployment(series='trusty', + openstack='cloud:trusty-juno', + source='cloud:trusty-updates/juno', + git=True) + deployment.run_tests() diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 6fee544d..acde6898 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -2,6 +2,7 @@ import amulet import time +import yaml from charmhelpers.contrib.openstack.amulet.deployment import ( OpenStackAmuletDeployment @@ -24,10 +25,12 @@ u = OpenStackAmuletUtils(ERROR) class NeutronOVSBasicDeployment(OpenStackAmuletDeployment): """Amulet tests on a basic neutron-openvswtich deployment.""" - def __init__(self, series, openstack=None, source=None, stable=False): + def __init__(self, series, openstack=None, source=None, git=False, + stable=False): """Deploy the entire test environment.""" super(NeutronOVSBasicDeployment, self).__init__(series, openstack, source, stable) + self.git = git self._add_services() self._add_relations() self._configure_services() @@ -61,7 +64,24 @@ class NeutronOVSBasicDeployment(OpenStackAmuletDeployment): def _configure_services(self): """Configure all of the services.""" - configs = {} + neutron_ovs_config = {} + if self.git: + branch = 'stable/' + self._get_openstack_release_string() + openstack_origin_git = { + 'repositories': [ + {'name': 'requirements', + 'repository': 'git://git.openstack.org/openstack/requirements', + 'branch': branch}, + {'name': 'neutron', + 'repository': 'git://git.openstack.org/openstack/neutron', + 'branch': branch}, + ], + 'directory': '/mnt/openstack-git', + 'http_proxy': 'http://squid.internal:3128', + 'https_proxy': 'https://squid.internal:3128', + } + neutron_ovs_config['openstack-origin-git'] = yaml.dump(openstack_origin_git) + configs = {'neutron-openvswitch': neutron_ovs_config} super(NeutronOVSBasicDeployment, self)._configure_services(configs) def _initialize_tests(self): @@ -76,7 +96,8 @@ class NeutronOVSBasicDeployment(OpenStackAmuletDeployment): service units.""" commands = { - self.compute_sentry: ['status nova-compute'], + self.compute_sentry: ['status nova-compute', + 'status neutron-plugin-openvswitch-agent'], self.rabbitmq_sentry: ['service rabbitmq-server status'], self.neutron_api_sentry: ['status neutron-server'], } diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, ('precise', 'cloud:precise-havana'): self.precise_havana, ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, - ('trusty', None): self.trusty_icehouse} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index 415b2110..43aa3614 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -1,2 +1,4 @@ import sys + +sys.path.append('actions/') sys.path.append('hooks/') diff --git a/unit_tests/test_actions_git_reinstall.py b/unit_tests/test_actions_git_reinstall.py new file mode 100644 index 00000000..76da5218 --- /dev/null +++ b/unit_tests/test_actions_git_reinstall.py @@ -0,0 +1,85 @@ +from mock import patch + +with patch('charmhelpers.core.hookenv.config') as config: + config.return_value = 'neutron' + import neutron_ovs_utils as utils # noqa + +import git_reinstall + +from test_utils import ( + CharmTestCase +) + +TO_PATCH = [ + 'config', +] + + +openstack_origin_git = \ + """repositories: + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements', + branch: stable/juno} + - {name: neutron, + repository: 'git://git.openstack.org/openstack/neutron', + branch: stable/juno}""" + + +class TestNeutronOVSActions(CharmTestCase): + + def setUp(self): + super(TestNeutronOVSActions, self).setUp(git_reinstall, TO_PATCH) + self.config.side_effect = self.test_config.get + + @patch.object(git_reinstall, 'action_set') + @patch.object(git_reinstall, 'action_fail') + @patch.object(git_reinstall, 'git_install') + def test_git_reinstall(self, git_install, action_fail, action_set): + self.test_config.set('openstack-origin-git', openstack_origin_git) + + git_reinstall.git_reinstall() + + git_install.assert_called_with(openstack_origin_git) + self.assertTrue(git_install.called) + self.assertFalse(action_set.called) + self.assertFalse(action_fail.called) + + @patch.object(git_reinstall, 'action_set') + @patch.object(git_reinstall, 'action_fail') + @patch.object(git_reinstall, 'git_install') + @patch('charmhelpers.contrib.openstack.utils.config') + def test_git_reinstall_not_configured(self, _config, git_install, + action_fail, action_set): + _config.return_value = None + + git_reinstall.git_reinstall() + + msg = 'openstack-origin-git is not configured' + action_fail.assert_called_with(msg) + self.assertFalse(git_install.called) + self.assertFalse(action_set.called) + + @patch.object(git_reinstall, 'action_set') + @patch.object(git_reinstall, 'action_fail') + @patch.object(git_reinstall, 'git_install') + @patch('charmhelpers.contrib.openstack.utils.config') + def test_git_reinstall_exception(self, _config, git_install, + action_fail, action_set): + _config.return_value = openstack_origin_git + e = OSError('something bad happened') + git_install.side_effect = e + traceback = ( + "Traceback (most recent call last):\n" + " File \"actions/git_reinstall.py\", line 33, in git_reinstall\n" + " git_install(config(\'openstack-origin-git\'))\n" + " File \"/usr/lib/python2.7/dist-packages/mock.py\", line 964, in __call__\n" # noqa + " return _mock_self._mock_call(*args, **kwargs)\n" + " File \"/usr/lib/python2.7/dist-packages/mock.py\", line 1019, in _mock_call\n" # noqa + " raise effect\n" + "OSError: something bad happened\n") + + git_reinstall.git_reinstall() + + msg = 'git-reinstall resulted in an unexpected error' + action_fail.assert_called_with(msg) + action_set.assert_called_with({'traceback': traceback}) diff --git a/unit_tests/test_neutron_ovs_context.py b/unit_tests/test_neutron_ovs_context.py index a7da378b..72fb5543 100644 --- a/unit_tests/test_neutron_ovs_context.py +++ b/unit_tests/test_neutron_ovs_context.py @@ -1,27 +1,30 @@ 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 = [ - 'relation_get', - 'relation_ids', - 'related_units', + 'resolve_address', 'config', 'unit_get', - 'add_bridge', - 'add_bridge_port', - 'service_running', - 'service_start', 'get_host_ip', ] +def fake_context(settings): + def outer(): + def inner(): + return settings + return inner + return outer + + class OVSPluginContextTest(CharmTestCase): def setUp(self): super(OVSPluginContextTest, self).setUp(context, TO_PATCH) - self.relation_get.side_effect = self.test_relation.get self.config.side_effect = self.test_config.get self.test_config.set('debug', True) self.test_config.set('verbose', True) @@ -30,49 +33,41 @@ class OVSPluginContextTest(CharmTestCase): def tearDown(self): super(OVSPluginContextTest, self).tearDown() + @patch('charmhelpers.contrib.openstack.context.config') @patch('charmhelpers.contrib.openstack.context.NeutronPortContext.' 'resolve_ports') - def test_data_port_name(self, mock_resolve_ports): + def test_data_port_name(self, mock_resolve_ports, config): 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.object(context, 'get_nic_hwaddr') + @patch('charmhelpers.contrib.openstack.context.config') @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): + def test_data_port_mac(self, list_nics, get_nic_hwaddr, config): 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 = ("br-d1:%s br-d2:%s" % (absent_mac, machine_machs['em1'])) self.test_config.set('data-port', config_macs) + 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.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') - - 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') + @patch.object(charmhelpers.contrib.openstack.context, 'related_units') @patch.object(charmhelpers.contrib.openstack.context, 'config') @patch.object(charmhelpers.contrib.openstack.context, 'unit_get') @patch.object(charmhelpers.contrib.openstack.context, 'is_clustered') @@ -84,7 +79,7 @@ class OVSPluginContextTest(CharmTestCase): @patch.object(charmhelpers.contrib.openstack.context, 'unit_private_ip') def test_neutroncc_context_api_rel(self, _unit_priv_ip, _npa, _ens_pkgs, _save_ff, _https, _is_clus, _unit_get, - _config): + _config, _runits, _rids, _rget): def mock_npa(plugin, section, manager): if section == "driver": return "neutron.randomdriver" @@ -95,19 +90,22 @@ class OVSPluginContextTest(CharmTestCase): _unit_get.return_value = '127.0.0.13' _unit_priv_ip.return_value = '127.0.0.14' _is_clus.return_value = False - self.related_units.return_value = ['unit1'] - 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', - }) + _runits.return_value = ['unit1'] + _rids.return_value = ['rid2'] + rdata = { + 'neutron-security-groups': 'True', + '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, @@ -126,8 +124,10 @@ 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') + @patch.object(charmhelpers.contrib.openstack.context, 'related_units') @patch.object(charmhelpers.contrib.openstack.context, 'config') @patch.object(charmhelpers.contrib.openstack.context, 'unit_get') @patch.object(charmhelpers.contrib.openstack.context, 'is_clustered') @@ -142,7 +142,8 @@ class OVSPluginContextTest(CharmTestCase): _ens_pkgs, _save_ff, _https, _is_clus, _unit_get, - _config): + _config, _runits, + _rids, _rget): def mock_npa(plugin, section, manager): if section == "driver": return "neutron.randomdriver" @@ -155,17 +156,19 @@ class OVSPluginContextTest(CharmTestCase): _unit_priv_ip.return_value = '127.0.0.14' _is_clus.return_value = False self.test_config.set('disable-security-groups', True) - self.related_units.return_value = ['unit1'] - 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', - }) + _runits.return_value = ['unit1'] + _rids.return_value = ['rid2'] + rdata = { + 'neutron-security-groups': 'True', + 'l2-population': 'True', + 'network-device-mtu': 1500, + 'overlay-network-type': 'gre', + } + _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, @@ -186,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..342b7c8c 100644 --- a/unit_tests/test_neutron_ovs_hooks.py +++ b/unit_tests/test_neutron_ovs_hooks.py @@ -1,7 +1,7 @@ - from mock import MagicMock, patch, call -from test_utils import CharmTestCase +import yaml +from test_utils import CharmTestCase with patch('charmhelpers.core.hookenv.config') as config: config.return_value = 'neutron' @@ -21,11 +21,18 @@ utils.restart_map = _map TO_PATCH = [ 'apt_update', 'apt_install', + 'apt_purge', 'config', 'CONFIGS', 'determine_packages', + 'determine_dvr_packages', + 'get_shared_secret', + 'git_install', 'log', + 'relation_ids', 'relation_set', + 'configure_ovs', + 'use_dvr', ] NEUTRON_CONF_DIR = "/etc/neutron" @@ -44,7 +51,9 @@ class NeutronOVSHooksTests(CharmTestCase): hooks.hooks.execute([ 'hooks/{}'.format(hookname)]) - def test_install_hook(self): + @patch.object(hooks, 'git_install_requested') + def test_install_hook(self, git_requested): + git_requested.return_value = False _pkgs = ['foo', 'bar'] self.determine_packages.return_value = [_pkgs] self._call_hook('install') @@ -53,9 +62,97 @@ class NeutronOVSHooksTests(CharmTestCase): call(_pkgs, fatal=True), ]) - def test_config_changed(self): + @patch.object(hooks, 'git_install_requested') + def test_install_hook_git(self, git_requested): + git_requested.return_value = True + _pkgs = ['foo', 'bar'] + self.determine_packages.return_value = _pkgs + openstack_origin_git = { + 'repositories': [ + {'name': 'requirements', + 'repository': 'git://git.openstack.org/openstack/requirements', # noqa + 'branch': 'stable/juno'}, + {'name': 'neutron', + 'repository': 'git://git.openstack.org/openstack/neutron', + 'branch': 'stable/juno'} + ], + 'directory': '/mnt/openstack-git', + } + projects_yaml = yaml.dump(openstack_origin_git) + self.test_config.set('openstack-origin-git', projects_yaml) + self._call_hook('install') + self.apt_update.assert_called_with() + self.assertTrue(self.determine_packages) + self.git_install.assert_called_with(projects_yaml) + + @patch.object(hooks, 'git_install_requested') + def test_config_changed(self, git_requested): + git_requested.return_value = False + self.relation_ids.return_value = ['relid'] + _zmq_joined = self.patch('zeromq_configuration_relation_joined') self._call_hook('config-changed') self.assertTrue(self.CONFIGS.write_all.called) + self.assertTrue(_zmq_joined.called_with('relid')) + self.configure_ovs.assert_called_with() + + @patch.object(hooks, 'git_install_requested') + @patch.object(hooks, 'config_value_changed') + def test_config_changed_git(self, config_val_changed, git_requested): + git_requested.return_value = True + self.relation_ids.return_value = ['relid'] + _zmq_joined = self.patch('zeromq_configuration_relation_joined') + openstack_origin_git = { + 'repositories': [ + {'name': 'requirements', + 'repository': + 'git://git.openstack.org/openstack/requirements', + 'branch': 'stable/juno'}, + {'name': 'neutron', + 'repository': 'git://git.openstack.org/openstack/neutron', + 'branch': 'stable/juno'} + ], + 'directory': '/mnt/openstack-git', + } + projects_yaml = yaml.dump(openstack_origin_git) + self.test_config.set('openstack-origin-git', projects_yaml) + self._call_hook('config-changed') + self.git_install.assert_called_with(projects_yaml) + self.assertTrue(self.CONFIGS.write_all.called) + self.assertTrue(_zmq_joined.called_with('relid')) + self.configure_ovs.assert_called_with() + + @patch.object(hooks, 'git_install_requested') + def test_config_changed_dvr(self, git_requested): + git_requested.return_value = False + 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') + + @patch.object(hooks, 'git_install_requested') + def test_neutron_plugin_joined(self, git_requested): + git_requested.return_value = False + 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..c75d7915 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,12 +16,28 @@ 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' +openstack_origin_git = \ + """repositories: + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements', + branch: stable/juno} + - {name: neutron, + repository: 'git://git.openstack.org/openstack/neutron', + branch: stable/juno}""" + def _mock_npa(plugin, attr, net_manager=None): plugins = { @@ -38,26 +55,42 @@ 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(nutils, 'git_install_requested') @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, _git_requested, + _use_dvr): + _git_requested.return_value = False + _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 +100,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 +109,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 +142,170 @@ 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') + + @patch.object(nutils, 'git_install_requested') + @patch.object(nutils, 'git_clone_and_install') + @patch.object(nutils, 'git_post_install') + @patch.object(nutils, 'git_pre_install') + def test_git_install(self, git_pre, git_post, git_clone_and_install, + git_requested): + projects_yaml = openstack_origin_git + git_requested.return_value = True + nutils.git_install(projects_yaml) + self.assertTrue(git_pre.called) + git_clone_and_install.assert_called_with(openstack_origin_git, + core_project='neutron') + self.assertTrue(git_post.called) + + @patch.object(nutils, 'mkdir') + @patch.object(nutils, 'write_file') + @patch.object(nutils, 'add_user_to_group') + @patch.object(nutils, 'add_group') + @patch.object(nutils, 'adduser') + def test_git_pre_install(self, adduser, add_group, add_user_to_group, + write_file, mkdir): + nutils.git_pre_install() + adduser.assert_called_with('neutron', shell='/bin/bash', + system_user=True) + add_group.assert_called_with('neutron', system_group=True) + add_user_to_group.assert_called_with('neutron', 'neutron') + expected = [ + call('/etc/neutron', owner='neutron', + group='neutron', perms=0700, force=False), + call('/etc/neutron/rootwrap.d', owner='neutron', + group='neutron', perms=0700, force=False), + call('/var/lib/neutron', owner='neutron', + group='neutron', perms=0700, force=False), + call('/var/lib/neutron/lock', owner='neutron', + group='neutron', perms=0700, force=False), + call('/var/log/neutron', owner='neutron', + group='neutron', perms=0700, force=False), + ] + self.assertEquals(mkdir.call_args_list, expected) + expected = [ + call('/var/log/neutron/server.log', '', owner='neutron', + group='neutron', perms=0600), + ] + self.assertEquals(write_file.call_args_list, expected) + + @patch.object(nutils, 'git_src_dir') + @patch.object(nutils, 'service_restart') + @patch.object(nutils, 'render') + @patch('os.path.join') + @patch('shutil.copyfile') + def test_git_post_install(self, copyfile, join, render, + service_restart, git_src_dir): + projects_yaml = openstack_origin_git + join.return_value = 'joined-string' + nutils.git_post_install(projects_yaml) + expected = [ + call('joined-string', '/etc/neutron/rootwrap.d/debug.filters'), + call('joined-string', '/etc/neutron/policy.json'), + call('joined-string', '/etc/neutron/rootwrap.conf'), + ] + copyfile.assert_has_calls(expected, any_order=True) + neutron_ovs_agent_context = { + 'service_description': 'Neutron OpenvSwitch Plugin Agent', + 'charm_name': 'neutron-openvswitch', + 'process_name': 'neutron-openvswitch-agent', + 'cleanup_process_name': 'neutron-ovs-cleanup', + 'plugin_config': '/etc/neutron/plugins/ml2/ml2_conf.ini', + 'log_file': '/var/log/neutron/openvswitch-agent.log', + } + neutron_ovs_cleanup_context = { + 'service_description': 'Neutron OpenvSwitch Cleanup', + 'charm_name': 'neutron-openvswitch', + 'process_name': 'neutron-ovs-cleanup', + 'log_file': '/var/log/neutron/ovs-cleanup.log', + } + expected = [ + call('neutron_sudoers', '/etc/sudoers.d/neutron_sudoers', {}, + perms=0o440), + call('upstart/neutron-plugin-openvswitch-agent.upstart', + '/etc/init/neutron-plugin-openvswitch-agent.conf', + neutron_ovs_agent_context, perms=0o644), + call('upstart/neutron-ovs-cleanup.upstart', + '/etc/init/neutron-ovs-cleanup.conf', + neutron_ovs_cleanup_context, perms=0o644), + ] + self.assertEquals(render.call_args_list, expected) + expected = [ + call('neutron-plugin-openvswitch-agent'), + ] + self.assertEquals(service_restart.call_args_list, expected)