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 adf4df7c..65430cc1 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: @@ -23,9 +23,10 @@ test: # coreycb note: The -v should only be temporary until Amulet sends # raise_status() messages to stderr: # https://bugs.launchpad.net/amulet/+bug/1320357 - @juju test -v -p AMULET_HTTP_PROXY --timeout 900 \ + @juju test -e trusty -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-api diff --git a/README.md b/README.md index e73aafd9..190c51eb 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,93 @@ This charm also supports scale out and high availability using the hacluster cha juju set neutron-api vip= juju add-relation neutron-hacluster neutron-api +# 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}" + # Restrictions This charm only support deployment with OpenStack Icehouse or better. diff --git a/actions.yaml b/actions.yaml new file mode 100644 index 00000000..27ef55b8 --- /dev/null +++ b/actions.yaml @@ -0,0 +1,2 @@ +git-reinstall: + description: Reinstall neutron-api 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..5f05b47a --- /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_api_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/config.yaml b/config.yaml index f4b5e732..77d7666c 100644 --- a/config.yaml +++ b/config.yaml @@ -14,6 +14,22 @@ options: Note that updating this setting to a source that is known to provide a later version of OpenStack will trigger a software upgrade. + + Note that when openstack-origin-git is specified, openstack + specific packages will be installed from source rather than + from the openstack-origin repository. + 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. + + Note that the installed config files will be determined based on + the OpenStack release of the openstack-origin option. + + For more details see README.md. rabbit-user: default: neutron type: string @@ -285,6 +301,28 @@ options: juju-myservice-0 If you're running multiple environments with the same services in them this allows you to differentiate between them. + enable-dvr: + default: False + type: boolean + description: | + Enable Distributed Virtual Routing (juno and above). + enable-l3ha: + default: False + type: boolean + description: | + Enable L3 HA (juno and above). + max-l3-agents-per-router: + default: 2 + type: int + description: | + Maximum number of l3 agents to host a router. Only used when enable-l3ha + is True + min-l3-agents-per-router: + default: 2 + type: int + description: | + Minimum number of l3 agents to host a router. Only used when enable-l3ha + is True nagios_servicegroups: default: "" type: string 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_api_context.py b/hooks/neutron_api_context.py index 2c488803..f0ff0d33 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -3,12 +3,16 @@ from charmhelpers.core.hookenv import ( relation_ids, related_units, relation_get, + log, ) from charmhelpers.contrib.openstack import context from charmhelpers.contrib.hahelpers.cluster import ( determine_api_port, determine_apache_port, ) +from charmhelpers.contrib.openstack.utils import ( + os_release, +) def get_l2population(): @@ -23,6 +27,43 @@ def get_overlay_network_type(): return overlay_net +def get_l3ha(): + if config('enable-l3ha'): + if os_release('neutron-server') < 'juno': + log('Disabling L3 HA, enable-l3ha is not valid before Juno') + return False + if config('overlay-network-type') not in ['vlan', 'gre', 'vxlan']: + log('Disabling L3 HA, enable-l3ha requires the use of the vxlan, ' + 'vlan or gre overlay network') + return False + if get_l2population(): + log('Disabling L3 HA, l2-population must be disabled with L3 HA') + return False + return True + else: + return False + + +def get_dvr(): + if config('enable-dvr'): + if os_release('neutron-server') < 'juno': + log('Disabling DVR, enable-dvr is not valid before Juno') + return False + if config('overlay-network-type') != 'vxlan': + log('Disabling DVR, enable-dvr requires the use of the vxlan ' + 'overlay network') + return False + if get_l3ha(): + log('Disabling DVR, enable-l3ha must be disabled with dvr') + return False + if not get_l2population(): + log('Disabling DVR, l2-population must be enabled to use dvr') + return False + return True + else: + return False + + class ApacheSSLContext(context.ApacheSSLContext): interfaces = ['https'] @@ -69,6 +110,14 @@ class NeutronCCContext(context.NeutronContext): def neutron_overlay_network_type(self): return get_overlay_network_type() + @property + def neutron_dvr(self): + return get_dvr() + + @property + def neutron_l3ha(self): + return get_l3ha() + # Do not need the plugin agent installed on the api server def _ensure_packages(self): pass @@ -91,6 +140,13 @@ class NeutronCCContext(context.NeutronContext): ctxt['nsx_controllers_list'] = \ config('nsx-controllers').split() ctxt['l2_population'] = self.neutron_l2_population + ctxt['enable_dvr'] = self.neutron_dvr + ctxt['l3_ha'] = self.neutron_l3ha + if self.neutron_l3ha: + ctxt['max_l3_agents_per_router'] = \ + config('max-l3-agents-per-router') + ctxt['min_l3_agents_per_router'] = \ + config('min-l3-agents-per-router') ctxt['overlay_network_type'] = self.neutron_overlay_network_type ctxt['external_network'] = config('neutron-external-network') ctxt['verbose'] = config('verbose') diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index 5c52b3e9..2abab2ee 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -2,8 +2,10 @@ import sys import uuid +from subprocess import ( + check_call, +) -from subprocess import check_call from charmhelpers.core.hookenv import ( Hooks, UnregisteredHookError, @@ -20,6 +22,7 @@ from charmhelpers.core.hookenv import ( from charmhelpers.core.host import ( restart_on_change, + service_reload, ) from charmhelpers.fetch import ( @@ -29,8 +32,11 @@ from charmhelpers.fetch import ( ) from charmhelpers.contrib.openstack.utils import ( + config_value_changed, configure_installation_source, + git_install_requested, openstack_upgrade_available, + os_requires_version, sync_db_with_multi_ipv6_addresses ) @@ -40,14 +46,21 @@ from neutron_api_utils import ( determine_packages, determine_ports, do_openstack_upgrade, + git_install, + dvr_router_present, + l3ha_router_present, register_configs, restart_map, services, - setup_ipv6 + setup_ipv6, + get_topics, ) from neutron_api_context import ( + get_dvr, + get_l3ha, get_l2population, get_overlay_network_type, + IdentityServiceContext, ) from charmhelpers.contrib.hahelpers.cluster import ( @@ -92,6 +105,10 @@ def configure_https(): cmd = ['a2dissite', 'openstack_https_frontend'] check_call(cmd) + # TODO: improve this by checking if local CN certs are available + # first then checking reload status (see LP #1433114). + service_reload('apache2', restart_on_failure=True) + for rid in relation_ids('identity-service'): identity_joined(rid=rid) @@ -100,9 +117,13 @@ def configure_https(): def install(): execd_preinstall() configure_installation_source(config('openstack-origin')) + apt_update() apt_install(determine_packages(config('openstack-origin')), fatal=True) + + git_install(config('openstack-origin-git')) + [open_port(port) for port in determine_ports()] @@ -110,6 +131,16 @@ def install(): @hooks.hook('config-changed') @restart_on_change(restart_map(), stopstart=True) def config_changed(): + if l3ha_router_present() and not get_l3ha(): + e = ('Cannot disable Router HA while ha enabled routers exist. Please' + ' remove any ha routers') + log(e, level=ERROR) + raise Exception(e) + if dvr_router_present() and not get_dvr(): + e = ('Cannot disable dvr while dvr enabled routers exist. Please' + ' remove any distributed routers') + log(e, level=ERROR) + raise Exception(e) apt_install(filter_installed_packages( determine_packages(config('openstack-origin'))), fatal=True) @@ -119,8 +150,12 @@ def config_changed(): config('database-user')) global CONFIGS - if openstack_upgrade_available('neutron-server'): - do_openstack_upgrade(CONFIGS) + if git_install_requested(): + if config_value_changed('openstack-origin-git'): + git_install(config('openstack-origin-git')) + else: + if openstack_upgrade_available('neutron-server'): + do_openstack_upgrade(CONFIGS) configure_https() update_nrpe_config() CONFIGS.write_all() @@ -132,6 +167,8 @@ def config_changed(): amqp_joined(relation_id=r_id) for r_id in relation_ids('identity-service'): identity_joined(rid=r_id) + for rid in relation_ids('zeromq-configuration'): + zeromq_configuration_relation_joined(rid) [cluster_joined(rid) for rid in relation_ids('cluster')] @@ -235,6 +272,8 @@ def identity_changed(): CONFIGS.write(NEUTRON_CONF) for r_id in relation_ids('neutron-api'): neutron_api_relation_joined(rid=r_id) + for r_id in relation_ids('neutron-plugin-api'): + neutron_plugin_api_relation_joined(rid=r_id) configure_https() @@ -278,6 +317,8 @@ def neutron_plugin_api_relation_joined(rid=None): relation_data = { 'neutron-security-groups': config('neutron-security-groups'), 'l2-population': get_l2population(), + 'enable-dvr': get_dvr(), + 'enable-l3ha': get_l3ha(), 'overlay-network-type': get_overlay_network_type(), } @@ -287,6 +328,23 @@ def neutron_plugin_api_relation_joined(rid=None): if net_dev_mtu: relation_data['network-device-mtu'] = net_dev_mtu + identity_ctxt = IdentityServiceContext()() + if not identity_ctxt: + identity_ctxt = {} + + relation_data.update({ + 'auth_host': identity_ctxt.get('auth_host'), + 'auth_port': identity_ctxt.get('auth_port'), + 'auth_protocol': identity_ctxt.get('auth_protocol'), + 'service_protocol': identity_ctxt.get('service_protocol'), + 'service_host': identity_ctxt.get('service_host'), + 'service_port': identity_ctxt.get('service_port'), + 'service_tenant': identity_ctxt.get('admin_tenant_name'), + 'service_username': identity_ctxt.get('admin_user'), + 'service_password': identity_ctxt.get('admin_password'), + 'region': config('region'), + }) + relation_set(relation_id=rid, **relation_data) @@ -381,6 +439,20 @@ def ha_changed(): neutron_api_relation_joined(rid=rid) +@hooks.hook('zeromq-configuration-relation-joined') +@os_requires_version('kilo', 'neutron-server') +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() + + @hooks.hook('nrpe-external-master-relation-joined', 'nrpe-external-master-relation-changed') def update_nrpe_config(): diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index 90d9a036..47e8d098 100644 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -1,6 +1,8 @@ from collections import OrderedDict from copy import deepcopy +from functools import partial import os +import shutil from base64 import b64encode from charmhelpers.contrib.openstack import context, templating from charmhelpers.contrib.openstack.neutron import ( @@ -10,6 +12,9 @@ from charmhelpers.contrib.openstack.neutron import ( from charmhelpers.contrib.openstack.utils import ( os_release, get_os_codename_install_source, + git_install_requested, + git_clone_and_install, + git_src_dir, configure_installation_source, ) @@ -26,9 +31,17 @@ from charmhelpers.fetch import ( ) from charmhelpers.core.host import ( - lsb_release + adduser, + add_group, + add_user_to_group, + mkdir, + lsb_release, + service_restart, + write_file, ) +from charmhelpers.core.templating import render + import neutron_api_context TEMPLATES = 'templates/' @@ -52,6 +65,29 @@ KILO_PACKAGES = [ 'python-neutron-vpnaas', ] +BASE_GIT_PACKAGES = [ + 'libxml2-dev', + 'libxslt1-dev', + 'python-dev', + 'python-pip', + 'python-setuptools', + 'zlib1g-dev', +] + +# ubuntu packages that should not be installed when deploying from git +GIT_PACKAGE_BLACKLIST = [ + 'neutron-server', + 'neutron-plugin-ml2', + 'python-keystoneclient', + 'python-six', +] + +GIT_PACKAGE_BLACKLIST_KILO = [ + 'python-neutron-lbaas', + 'python-neutron-fwaas', + 'python-neutron-vpnaas', +] + BASE_SERVICES = [ 'neutron-server' ] @@ -82,6 +118,8 @@ BASE_RESOURCE_MAP = OrderedDict([ service_user='neutron'), neutron_api_context.NeutronCCContext(), context.SyslogContext(), + context.ZeroMQContext(), + context.NotificationDriverContext(), context.BindHostContext(), context.WorkerConfigContext()], }), @@ -112,14 +150,27 @@ def api_port(service): def determine_packages(source=None): # currently all packages match service names packages = [] + BASE_PACKAGES + for v in resource_map().values(): packages.extend(v['services']) pkgs = neutron_plugin_attribute(config('neutron-plugin'), 'server_packages', 'neutron') packages.extend(pkgs) + if get_os_codename_install_source(source) >= 'kilo': packages.extend(KILO_PACKAGES) + + if git_install_requested(): + packages.extend(BASE_GIT_PACKAGES) + # don't include packages that will be installed from git + packages = list(set(packages)) + for p in GIT_PACKAGE_BLACKLIST: + packages.remove(p) + if get_os_codename_install_source(source) >= 'kilo': + for p in GIT_PACKAGE_BLACKLIST_KILO: + packages.remove(p) + return list(set(packages)) @@ -230,6 +281,16 @@ def do_openstack_upgrade(configs): configs.set_release(openstack_release=new_os_rel) +def get_topics(): + return ['q-l3-plugin', + 'q-firewall-plugin', + 'n-lbaas-plugin', + 'ipsec_driver', + 'q-metering-plugin', + 'q-plugin', + 'neutron'] + + def setup_ipv6(): ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower() if ubuntu_rel < "trusty": @@ -244,3 +305,104 @@ def setup_ipv6(): ' main') apt_update() apt_install('haproxy/trusty-backports', fatal=True) + + +def router_feature_present(feature): + ''' Check For dvr enabled routers ''' + env = neutron_api_context.IdentityServiceContext()() + if not env: + log('Unable to check resources at this time') + return + + auth_url = '%(auth_protocol)s://%(auth_host)s:%(auth_port)s/v2.0' % env + # Late import to avoid install hook failures when pkg hasnt been installed + from neutronclient.v2_0 import client + neutron_client = client.Client(username=env['admin_user'], + password=env['admin_password'], + tenant_name=env['admin_tenant_name'], + auth_url=auth_url, + region_name=env['region']) + for router in neutron_client.list_routers()['routers']: + if router.get(feature, False): + return True + return False + +l3ha_router_present = partial(router_feature_present, feature='ha') + +dvr_router_present = partial(router_feature_present, feature='distributed') + + +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', + '/etc/neutron/plugins', + '/etc/neutron/plugins/ml2', + '/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 = { + 'api-paste': { + 'src': os.path.join(src_etc, 'api-paste.ini'), + 'dest': '/etc/neutron/api-paste.ini', + }, + '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_api_context = { + 'service_description': 'Neutron API server', + 'charm_name': 'neutron-api', + 'process_name': 'neutron-server', + } + + # NOTE(coreycb): Needs systemd support + render('upstart/neutron-server.upstart', '/etc/init/neutron-server.conf', + neutron_api_context, perms=0o644) + + service_restart('neutron-server') diff --git a/hooks/zeromq-configuration-relation-changed b/hooks/zeromq-configuration-relation-changed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/zeromq-configuration-relation-changed @@ -0,0 +1 @@ +neutron_api_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..1fb10fd5 --- /dev/null +++ b/hooks/zeromq-configuration-relation-joined @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 773a75ad..ffe55e09 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -34,6 +34,9 @@ requires: ha: interface: hacluster scope: container + zeromq-configuration: + interface: zeromq-configuration + scope: container peers: cluster: interface: neutron-api-ha diff --git a/templates/icehouse/neutron.conf b/templates/icehouse/neutron.conf index 41b6171a..c78169d5 100644 --- a/templates/icehouse/neutron.conf +++ b/templates/icehouse/neutron.conf @@ -1,3 +1,4 @@ +# icehouse ############################################################################### # [ WARNING ] # Configuration file maintained by Juju. Local changes may be overwritten. @@ -11,7 +12,10 @@ state_path = /var/lib/neutron lock_path = $state_path/lock bind_host = {{ bind_host }} auth_strategy = keystone + +{% if notifications == 'True' -%} notification_driver = neutron.openstack.common.notifier.rpc_notifier +{% endif -%} api_workers = {{ workers }} rpc_workers = {{ workers }} diff --git a/templates/juno/neutron.conf b/templates/juno/neutron.conf new file mode 100644 index 00000000..05d3e212 --- /dev/null +++ b/templates/juno/neutron.conf @@ -0,0 +1,85 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +## Restart trigger {{ restart_trigger }} +############################################################################### +[DEFAULT] +verbose = {{ verbose }} +debug = {{ debug }} +use_syslog = {{ use_syslog }} +state_path = /var/lib/neutron +lock_path = $state_path/lock +bind_host = {{ bind_host }} +auth_strategy = keystone +notification_driver = neutron.openstack.common.notifier.rpc_notifier +api_workers = {{ workers }} +rpc_workers = {{ workers }} + +router_distributed = {{ enable_dvr }} + +l3_ha = {{ l3_ha }} +{% if l3_ha -%} +max_l3_agents_per_router = {{ max_l3_agents_per_router }} +min_l3_agents_per_router = {{ min_l3_agents_per_router }} +{% endif -%} + +{% if neutron_bind_port -%} +bind_port = {{ neutron_bind_port }} +{% else -%} +bind_port = 9696 +{% endif -%} + +{% if core_plugin -%} +core_plugin = {{ core_plugin }} +{% if neutron_plugin in ['ovs', 'ml2'] -%} +service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,neutron.services.firewall.fwaas_plugin.FirewallPlugin,neutron.services.loadbalancer.plugin.LoadBalancerPlugin,neutron.services.vpn.plugin.VPNDriverPlugin,neutron.services.metering.metering_plugin.MeteringPlugin +{% endif -%} +{% endif -%} + +{% if neutron_security_groups -%} +allow_overlapping_ips = True +neutron_firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver +{% endif -%} + +{% include "parts/rabbitmq" %} + +notify_nova_on_port_status_changes = True +notify_nova_on_port_data_changes = True +nova_url = {{ nova_url }} +nova_region_name = {{ region }} +{% if auth_host -%} +nova_admin_username = {{ admin_user }} +nova_admin_tenant_id = {{ admin_tenant_id }} +nova_admin_password = {{ admin_password }} +nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 +{% endif -%} + +[quotas] +quota_driver = neutron.db.quota_db.DbQuotaDriver +{% if neutron_security_groups -%} +quota_items = network,subnet,port,security_group,security_group_rule +{% endif -%} + +[agent] +root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf + +[keystone_authtoken] +signing_dir = {{ signing_dir }} +{% if service_host -%} +service_protocol = {{ service_protocol }} +service_host = {{ service_host }} +service_port = {{ service_port }} +auth_host = {{ auth_host }} +auth_port = {{ auth_port }} +auth_protocol = {{ auth_protocol }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +{% endif -%} + +{% include "parts/section-database" %} + +[service_providers] +service_provider=LOADBALANCER:Haproxy:neutron.services.loadbalancer.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default +service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default +service_provider=FIREWALL:Iptables:neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver:default diff --git a/templates/kilo/neutron.conf b/templates/kilo/neutron.conf index 12955111..a6a3c664 100644 --- a/templates/kilo/neutron.conf +++ b/templates/kilo/neutron.conf @@ -1,3 +1,4 @@ +# kilo ############################################################################### # [ WARNING ] # Configuration file maintained by Juju. Local changes may be overwritten. @@ -8,13 +9,20 @@ verbose = {{ verbose }} debug = {{ debug }} use_syslog = {{ use_syslog }} state_path = /var/lib/neutron -lock_path = $state_path/lock bind_host = {{ bind_host }} auth_strategy = keystone notification_driver = neutron.openstack.common.notifier.rpc_notifier api_workers = {{ workers }} rpc_workers = {{ workers }} +router_distributed = {{ enable_dvr }} + +l3_ha = {{ l3_ha }} +{% if l3_ha -%} +max_l3_agents_per_router = {{ max_l3_agents_per_router }} +min_l3_agents_per_router = {{ min_l3_agents_per_router }} +{% endif -%} + {% if neutron_bind_port -%} bind_port = {{ neutron_bind_port }} {% else -%} @@ -33,8 +41,6 @@ allow_overlapping_ips = True neutron_firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver {% endif -%} -{% include "parts/rabbitmq" %} - notify_nova_on_port_status_changes = True notify_nova_on_port_data_changes = True nova_url = {{ nova_url }} @@ -46,6 +52,8 @@ nova_admin_password = {{ admin_password }} nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 {% endif -%} +{% include "section-zeromq" %} + [quotas] quota_driver = neutron.db.quota_db.DbQuotaDriver {% if neutron_security_groups -%} @@ -56,7 +64,7 @@ quota_security_group_rule = {{ quota_security_group_rule }} quota_items = network,subnet,port {% endif -%} quota_network = {{ quota_network }} -quota_subnet = {{ quota_subnet }} +quota_subnet = {{ quota_subnet }} quota_port = {{ quota_port }} quota_vip = {{ quota_vip }} quota_pool = {{ quota_pool }} @@ -68,23 +76,16 @@ quota_floatingip = {{ quota_floatingip }} [agent] root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf -[keystone_authtoken] -signing_dir = {{ signing_dir }} -{% if service_host -%} -service_protocol = {{ service_protocol }} -service_host = {{ service_host }} -service_port = {{ service_port }} -auth_host = {{ auth_host }} -auth_port = {{ auth_port }} -auth_protocol = {{ auth_protocol }} -admin_tenant_name = {{ admin_tenant_name }} -admin_user = {{ admin_user }} -admin_password = {{ admin_password }} -{% endif -%} +{% include "section-keystone-authtoken" %} {% include "parts/section-database" %} +{% include "section-rabbitmq-oslo" %} + [service_providers] service_provider=LOADBALANCER:Haproxy:neutron_lbaas.services.loadbalancer.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default service_provider=VPN:openswan:neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default service_provider=FIREWALL:Iptables:neutron_fwaas.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver:default + +[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-server.upstart b/templates/upstart/neutron-server.upstart new file mode 100644 index 00000000..7211e129 --- /dev/null +++ b/templates/upstart/neutron-server.upstart @@ -0,0 +1,22 @@ +description "{{ service_description }}" +author "Juju {{ charm_name }} Charm " + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn + +chdir /var/run + +pre-start script + mkdir -p /var/run/neutron + chown neutron:root /var/run/neutron +end script + +script + [ -r /etc/default/{{ process_name }} ] && . /etc/default/{{ process_name }} + [ -r "$NEUTRON_PLUGIN_CONFIG" ] && CONF_ARG="--config-file $NEUTRON_PLUGIN_CONFIG" + exec start-stop-daemon --start --chuid neutron --exec /usr/local/bin/neutron-server -- \ + --config-file /etc/neutron/neutron.conf \ + --log-file /var/log/neutron/server.log $CONF_ARG +end script diff --git a/tests/16-basic-trusty-icehouse-git b/tests/16-basic-trusty-icehouse-git new file mode 100755 index 00000000..51517017 --- /dev/null +++ b/tests/16-basic-trusty-icehouse-git @@ -0,0 +1,9 @@ +#!/usr/bin/python + +"""Amulet tests on a basic neutron-api git deployment on trusty-icehouse.""" + +from basic_deployment import NeutronAPIBasicDeployment + +if __name__ == '__main__': + deployment = NeutronAPIBasicDeployment(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..91c9bdbd --- /dev/null +++ b/tests/18-basic-trusty-juno-git @@ -0,0 +1,12 @@ +#!/usr/bin/python + +"""Amulet tests on a basic neutron-api git deployment on trusty-juno.""" + +from basic_deployment import NeutronAPIBasicDeployment + +if __name__ == '__main__': + deployment = NeutronAPIBasicDeployment(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 e65a1481..c611623c 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -1,6 +1,7 @@ #!/usr/bin/python import amulet +import yaml from charmhelpers.contrib.openstack.amulet.deployment import ( OpenStackAmuletDeployment @@ -19,10 +20,12 @@ u = OpenStackAmuletUtils(ERROR) class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): """Amulet tests on a basic neutron-api 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(NeutronAPIBasicDeployment, self).__init__(series, openstack, source, stable) + self.git = git self._add_services() self._add_relations() self._configure_services() @@ -65,11 +68,29 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): def _configure_services(self): """Configure all of the services.""" + neutron_api_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_api_config['openstack-origin-git'] = yaml.dump(openstack_origin_git) keystone_config = {'admin-password': 'openstack', 'admin-token': 'ubuntutesting'} nova_cc_config = {'network-manager': 'Quantum', 'quantum-security-groups': 'yes'} - configs = {'keystone': keystone_config, + configs = {'neutron-api': neutron_api_config, + 'keystone': keystone_config, 'nova-cloud-controller': nova_cc_config} super(NeutronAPIBasicDeployment, self)._configure_services(configs) @@ -178,7 +199,6 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): 'auth_host': id_ip, 'auth_port': "35357", 'auth_protocol': 'http', - 'https_keystone': "False", 'private-address': id_ip, 'service_host': id_ip, } @@ -294,7 +314,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): 'nova_admin_auth_url': nova_auth_url, }, 'keystone_authtoken': { - 'signing_dir': '/var/lib/neutron/keystone-signing', + 'signing_dir': '/var/cache/neutron', 'service_protocol': ks_rel['service_protocol'], 'service_host': ks_rel['service_host'], 'service_port': ks_rel['service_port'], @@ -355,6 +375,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): def test_services(self): """Verify the expected services are running on the corresponding service units.""" + neutron_api_services = ['status neutron-server'] neutron_services = ['status neutron-dhcp-agent', 'status neutron-lbaas-agent', 'status neutron-metadata-agent', @@ -374,7 +395,8 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment): self.mysql_sentry: ['status mysql'], self.keystone_sentry: ['status keystone'], self.nova_cc_sentry: nova_cc_services, - self.quantum_gateway_sentry: neutron_services + self.quantum_gateway_sentry: neutron_services, + self.neutron_api_sentry: neutron_api_services, } ret = u.validate_services(commands) 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..c520d25e --- /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_api_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 TestNeutronAPIActions(CharmTestCase): + + def setUp(self): + super(TestNeutronAPIActions, 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_api_context.py b/unit_tests/test_neutron_api_context.py index 60134bac..8fd91af4 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -3,15 +3,131 @@ from mock import patch import neutron_api_context as context import charmhelpers TO_PATCH = [ + 'config', + 'determine_api_port', + 'determine_apache_port', + 'log', + 'os_release', 'relation_get', 'relation_ids', 'related_units', - 'config', - 'determine_api_port', - 'determine_apache_port' ] +class GeneralTests(CharmTestCase): + def setUp(self): + super(GeneralTests, self).setUp(context, TO_PATCH) + self.relation_get.side_effect = self.test_relation.get + self.config.side_effect = self.test_config.get + + def test_l2population(self): + self.test_config.set('l2-population', True) + self.test_config.set('neutron-plugin', 'ovs') + self.assertEquals(context.get_l2population(), True) + + def test_l2population_nonovs(self): + self.test_config.set('l2-population', True) + self.test_config.set('neutron-plugin', 'nsx') + self.assertEquals(context.get_l2population(), False) + + def test_get_overlay_network_type(self): + self.test_config.set('overlay-network-type', 'gre') + self.assertEquals(context.get_overlay_network_type(), 'gre') + + def test_get_overlay_network_type_unsupported(self): + self.test_config.set('overlay-network-type', 'tokenring') + with self.assertRaises(Exception) as _exceptctxt: + context.get_overlay_network_type() + self.assertEqual(_exceptctxt.exception.message, + 'Unsupported overlay-network-type') + + def test_get_l3ha(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), True) + + def test_get_l3ha_prejuno(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'icehouse' + self.assertEquals(context.get_l3ha(), False) + + def test_get_l3ha_l2pop(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), False) + + def test_get_l3ha_badoverlay(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'tokenring') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), False) + + def test_get_dvr(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), True) + + def test_get_dvr_explicit_off(self): + self.test_config.set('enable-dvr', False) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_prejuno(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'icehouse' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_gre(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_l3ha_on(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_l2pop(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + class IdentityServiceContext(CharmTestCase): def setUp(self): @@ -152,6 +268,8 @@ class NeutronCCContextTest(CharmTestCase): plugin.return_value = None ctxt_data = { 'debug': True, + 'enable_dvr': False, + 'l3_ha': False, 'external_network': 'bob', 'neutron_bind_port': self.api_port, 'verbose': True, @@ -181,6 +299,8 @@ class NeutronCCContextTest(CharmTestCase): self.test_config.set('overlay-network-type', 'vxlan') ctxt_data = { 'debug': True, + 'enable_dvr': False, + 'l3_ha': False, 'external_network': 'bob', 'neutron_bind_port': self.api_port, 'verbose': True, @@ -202,6 +322,43 @@ class NeutronCCContextTest(CharmTestCase): with patch.object(napi_ctxt, '_ensure_packages'): self.assertEquals(ctxt_data, napi_ctxt()) + @patch.object(context.NeutronCCContext, 'network_manager') + @patch.object(context.NeutronCCContext, 'plugin') + @patch('__builtin__.__import__') + def test_neutroncc_context_l3ha(self, _import, plugin, nm): + plugin.return_value = None + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + ctxt_data = { + 'debug': True, + 'enable_dvr': False, + 'l3_ha': True, + 'external_network': 'bob', + 'neutron_bind_port': self.api_port, + 'verbose': True, + 'l2_population': False, + 'overlay_network_type': 'gre', + 'max_l3_agents_per_router': 2, + 'min_l3_agents_per_router': 2, + 'quota_floatingip': 50, + 'quota_health_monitors': -1, + 'quota_member': -1, + 'quota_network': 10, + 'quota_pool': 10, + 'quota_port': 50, + 'quota_router': 10, + 'quota_security_group': 10, + 'quota_security_group_rule': 100, + 'quota_subnet': 10, + 'quota_vip': 10, + } + napi_ctxt = context.NeutronCCContext() + with patch.object(napi_ctxt, '_ensure_packages'): + self.assertEquals(ctxt_data, napi_ctxt()) + @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('__builtin__.__import__') diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index 5bf61cf4..4a7c045f 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -1,4 +1,5 @@ from mock import MagicMock, patch, call +import yaml from test_utils import CharmTestCase @@ -32,14 +33,20 @@ TO_PATCH = [ 'determine_packages', 'determine_ports', 'do_openstack_upgrade', + 'dvr_router_present', + 'l3ha_router_present', 'execd_preinstall', 'filter_installed_packages', + 'get_dvr', + 'get_l3ha', 'get_l2population', 'get_overlay_network_type', + 'git_install', 'is_relation_made', 'log', 'open_port', 'openstack_upgrade_available', + 'os_requires_version', 'relation_get', 'relation_ids', 'relation_set', @@ -48,6 +55,8 @@ TO_PATCH = [ 'get_netmask_for_address', 'get_address_in_network', 'update_nrpe_config', + 'service_reload', + 'IdentityServiceContext', ] NEUTRON_CONF_DIR = "/etc/neutron" @@ -56,6 +65,15 @@ NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR from random import randrange +class DummyContext(): + + def __init__(self, return_value): + self.return_value = return_value + + def __call__(self): + return self.return_value + + class NeutronAPIHooksTests(CharmTestCase): def setUp(self): @@ -73,7 +91,9 @@ class NeutronAPIHooksTests(CharmTestCase): hooks.hooks.execute([ 'hooks/{}'.format(hookname)]) - def test_install_hook(self): + @patch.object(utils, 'git_install_requested') + def test_install_hook(self, git_requested): + git_requested.return_value = False _pkgs = ['foo', 'bar'] _ports = [80, 81, 82] _port_calls = [call(port) for port in _ports] @@ -90,9 +110,46 @@ class NeutronAPIHooksTests(CharmTestCase): self.open_port.assert_has_calls(_port_calls) self.assertTrue(self.execd_preinstall.called) + @patch.object(utils, 'git_install_requested') + def test_install_hook_git(self, git_requested): + git_requested.return_value = True + _pkgs = ['foo', 'bar'] + _ports = [80, 81, 82] + _port_calls = [call(port) for port in _ports] + self.determine_packages.return_value = _pkgs + self.determine_ports.return_value = _ports + repo = 'cloud:trusty-juno' + 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', repo) + self.test_config.set('openstack-origin-git', projects_yaml) + self._call_hook('install') + self.assertTrue(self.execd_preinstall.called) + self.configure_installation_source.assert_called_with(repo) + self.apt_update.assert_called_with() + self.apt_install.assert_has_calls([ + call(_pkgs, fatal=True), + ]) + self.git_install.assert_called_with(projects_yaml) + self.open_port.assert_has_calls(_port_calls) + @patch.object(hooks, 'configure_https') - def test_config_changed(self, conf_https): + @patch.object(hooks, 'git_install_requested') + def test_config_changed(self, git_requested, conf_https): + git_requested.return_value = False self.openstack_upgrade_available.return_value = True + self.dvr_router_present.return_value = False + self.l3ha_router_present.return_value = False self.relation_ids.side_effect = self._fake_relids _n_api_rel_joined = self.patch('neutron_api_relation_joined') _n_plugin_api_rel_joined =\ @@ -100,16 +157,64 @@ class NeutronAPIHooksTests(CharmTestCase): _amqp_rel_joined = self.patch('amqp_joined') _id_rel_joined = self.patch('identity_joined') _id_cluster_joined = self.patch('cluster_joined') + _zmq_joined = self.patch('zeromq_configuration_relation_joined') self._call_hook('config-changed') self.assertTrue(_n_api_rel_joined.called) self.assertTrue(_n_plugin_api_rel_joined.called) self.assertTrue(_amqp_rel_joined.called) self.assertTrue(_id_rel_joined.called) self.assertTrue(_id_cluster_joined.called) + self.assertTrue(_zmq_joined.called) self.assertTrue(self.CONFIGS.write_all.called) self.assertTrue(self.do_openstack_upgrade.called) self.assertTrue(self.apt_install.called) + @patch.object(hooks, 'configure_https') + @patch.object(hooks, 'git_install_requested') + @patch.object(hooks, 'config_value_changed') + def test_config_changed_git(self, config_val_changed, git_requested, + configure_https): + git_requested.return_value = True + self.dvr_router_present.return_value = False + self.l3ha_router_present.return_value = False + self.relation_ids.side_effect = self._fake_relids + _n_api_rel_joined = self.patch('neutron_api_relation_joined') + _n_plugin_api_rel_joined =\ + self.patch('neutron_plugin_api_relation_joined') + _amqp_rel_joined = self.patch('amqp_joined') + _id_rel_joined = self.patch('identity_joined') + _id_cluster_joined = self.patch('cluster_joined') + _zmq_joined = self.patch('zeromq_configuration_relation_joined') + repo = 'cloud:trusty-juno' + 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', repo) + self.test_config.set('openstack-origin-git', projects_yaml) + self._call_hook('config-changed') + self.git_install.assert_called_with(projects_yaml) + self.assertFalse(self.do_openstack_upgrade.called) + self.assertTrue(self.apt_install.called) + self.assertTrue(configure_https.called) + self.assertTrue(self.update_nrpe_config.called) + self.assertTrue(self.CONFIGS.write_all.called) + self.assertTrue(_n_api_rel_joined.called) + self.assertTrue(_n_plugin_api_rel_joined.called) + self.assertTrue(_amqp_rel_joined.called) + self.assertTrue(_id_rel_joined.called) + self.assertTrue(_zmq_joined.called) + self.assertTrue(_id_cluster_joined.called) + def test_amqp_joined(self): self._call_hook('amqp-relation-joined') self.relation_set.assert_called_with( @@ -272,11 +377,87 @@ class NeutronAPIHooksTests(CharmTestCase): self.assertTrue(self.CONFIGS.write.called_with(NEUTRON_CONF)) def test_neutron_plugin_api_relation_joined_nol2(self): + self.IdentityServiceContext.return_value = \ + DummyContext(return_value={}) _relation_data = { 'neutron-security-groups': False, + 'enable-dvr': False, + 'enable-l3ha': False, 'l2-population': False, 'overlay-network-type': 'vxlan', + 'service_protocol': None, + 'auth_protocol': None, + 'service_tenant': None, + 'service_port': None, + 'region': 'RegionOne', + 'service_password': None, + 'auth_port': None, + 'auth_host': None, + 'service_username': None, + 'service_host': None } + self.get_dvr.return_value = False + self.get_l3ha.return_value = False + self.get_l2population.return_value = False + self.get_overlay_network_type.return_value = 'vxlan' + self._call_hook('neutron-plugin-api-relation-joined') + self.relation_set.assert_called_with( + relation_id=None, + **_relation_data + ) + + def test_neutron_plugin_api_relation_joined_dvr(self): + self.IdentityServiceContext.return_value = \ + DummyContext(return_value={}) + _relation_data = { + 'neutron-security-groups': False, + 'enable-dvr': True, + 'enable-l3ha': False, + 'l2-population': True, + 'overlay-network-type': 'vxlan', + 'service_protocol': None, + 'auth_protocol': None, + 'service_tenant': None, + 'service_port': None, + 'region': 'RegionOne', + 'service_password': None, + 'auth_port': None, + 'auth_host': None, + 'service_username': None, + 'service_host': None + } + self.get_dvr.return_value = True + self.get_l3ha.return_value = False + self.get_l2population.return_value = True + self.get_overlay_network_type.return_value = 'vxlan' + self._call_hook('neutron-plugin-api-relation-joined') + self.relation_set.assert_called_with( + relation_id=None, + **_relation_data + ) + + def test_neutron_plugin_api_relation_joined_l3ha(self): + self.IdentityServiceContext.return_value = \ + DummyContext(return_value={}) + _relation_data = { + 'neutron-security-groups': False, + 'enable-dvr': False, + 'enable-l3ha': True, + 'l2-population': False, + 'overlay-network-type': 'vxlan', + 'service_protocol': None, + 'auth_protocol': None, + 'service_tenant': None, + 'service_port': None, + 'region': 'RegionOne', + 'service_password': None, + 'auth_port': None, + 'auth_host': None, + 'service_username': None, + 'service_host': None + } + self.get_dvr.return_value = False + self.get_l3ha.return_value = True self.get_l2population.return_value = False self.get_overlay_network_type.return_value = 'vxlan' self._call_hook('neutron-plugin-api-relation-joined') @@ -286,13 +467,29 @@ class NeutronAPIHooksTests(CharmTestCase): ) def test_neutron_plugin_api_relation_joined_w_mtu(self): + self.IdentityServiceContext.return_value = \ + DummyContext(return_value={}) self.test_config.set('network-device-mtu', 1500) _relation_data = { 'neutron-security-groups': False, 'l2-population': False, 'overlay-network-type': 'vxlan', 'network-device-mtu': 1500, + 'enable-l3ha': True, + 'enable-dvr': True, + 'service_protocol': None, + 'auth_protocol': None, + 'service_tenant': None, + 'service_port': None, + 'region': 'RegionOne', + 'service_password': None, + 'auth_port': None, + 'auth_host': None, + 'service_username': None, + 'service_host': None } + self.get_dvr.return_value = True + self.get_l3ha.return_value = True self.get_l2population.return_value = False self.get_overlay_network_type.return_value = 'vxlan' self._call_hook('neutron-plugin-api-relation-joined') @@ -433,8 +630,9 @@ class NeutronAPIHooksTests(CharmTestCase): self.relation_ids.side_effect = self._fake_relids _id_rel_joined = self.patch('identity_joined') hooks.configure_https() - self.check_call.assert_called_with(['a2ensite', - 'openstack_https_frontend']) + calls = [call('a2dissite', 'openstack_https_frontend'), + call('service', 'apache2', 'reload')] + self.check_call.assert_called_has_calls(calls) self.assertTrue(_id_rel_joined.called) def test_configure_https_nohttps(self): @@ -442,6 +640,7 @@ class NeutronAPIHooksTests(CharmTestCase): self.relation_ids.side_effect = self._fake_relids _id_rel_joined = self.patch('identity_joined') hooks.configure_https() - self.check_call.assert_called_with(['a2dissite', - 'openstack_https_frontend']) + calls = [call('a2dissite', 'openstack_https_frontend'), + call('service', 'apache2', 'reload')] + self.check_call.assert_called_has_calls(calls) self.assertTrue(_id_rel_joined.called) diff --git a/unit_tests/test_neutron_api_utils.py b/unit_tests/test_neutron_api_utils.py index 9ff7ef45..7dcaa04b 100644 --- a/unit_tests/test_neutron_api_utils.py +++ b/unit_tests/test_neutron_api_utils.py @@ -1,5 +1,5 @@ -from mock import MagicMock, patch +from mock import MagicMock, patch, call from collections import OrderedDict from copy import deepcopy import charmhelpers.contrib.openstack.templating as templating @@ -31,6 +31,15 @@ TO_PATCH = [ 'os_release', ] +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 = { @@ -64,13 +73,17 @@ class TestNeutronAPIUtils(CharmTestCase): port = nutils.api_port('neutron-server') self.assertEqual(port, nutils.API_PORTS['neutron-server']) - def test_determine_packages(self): + @patch.object(nutils, 'git_install_requested') + def test_determine_packages(self, git_requested): + git_requested.return_value = False pkg_list = nutils.determine_packages() expect = deepcopy(nutils.BASE_PACKAGES) expect.extend(['neutron-server', 'neutron-plugin-ml2']) self.assertItemsEqual(pkg_list, expect) - def test_determine_packages_kilo(self): + @patch.object(nutils, 'git_install_requested') + def test_determine_packages_kilo(self, git_requested): + git_requested.return_value = False self.get_os_codename_install_source.return_value = 'kilo' pkg_list = nutils.determine_packages() expect = deepcopy(nutils.BASE_PACKAGES) @@ -159,7 +172,9 @@ class TestNeutronAPIUtils(CharmTestCase): nutils.keystone_ca_cert_b64() self.assertTrue(self.b64encode.called) - def test_do_openstack_upgrade(self): + @patch.object(nutils, 'git_install_requested') + def test_do_openstack_upgrade(self, git_requested): + git_requested.return_value = False self.config.side_effect = self.test_config.get self.test_config.set('openstack-origin', 'cloud:trusty-juno') self.os_release.side_effect = 'icehouse' @@ -184,3 +199,87 @@ class TestNeutronAPIUtils(CharmTestCase): options=dpkg_opts, fatal=True) configs.set_release.assert_called_with(openstack_release='juno') + + @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('/etc/neutron/plugins', owner='neutron', + group='neutron', perms=0700, force=False), + call('/etc/neutron/plugins/ml2', 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/api-paste.ini'), + 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_api_context = { + 'service_description': 'Neutron API server', + 'charm_name': 'neutron-api', + 'process_name': 'neutron-server', + } + expected = [ + call('neutron_sudoers', '/etc/sudoers.d/neutron_sudoers', {}, + perms=0o440), + call('upstart/neutron-server.upstart', + '/etc/init/neutron-server.conf', + neutron_api_context, perms=0o644), + ] + self.assertEquals(render.call_args_list, expected) + expected = [ + call('neutron-server'), + ] + self.assertEquals(service_restart.call_args_list, expected)