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..0574b23e 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: @@ -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-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..a0b9e5fb --- /dev/null +++ b/actions/git_reinstall.py @@ -0,0 +1,45 @@ +#!/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, +) + +from neutron_api_hooks import ( + config_changed, +) + + +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')) + config_changed() + 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 654b9ced..212c9ced 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 diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py index fef96384..11d49a7c 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py @@ -101,7 +101,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, + self.utopic_juno, self.vivid_kilo) = range(10) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, @@ -110,7 +111,9 @@ class OpenStackAmuletDeployment(AmuletDeployment): ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, ('trusty', None): self.trusty_icehouse, ('trusty', 'cloud:trusty-juno'): self.trusty_juno, - ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, + ('utopic', None): self.utopic_juno, + ('vivid', None): self.vivid_kilo} return releases[(self.series, self.openstack)] def _get_openstack_release_string(self): diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 5a12c9d6..f90a0289 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -524,9 +524,10 @@ def git_clone_and_install(projects_yaml, core_project): projects = yaml.load(projects_yaml) _git_validate_projects_yaml(projects, core_project) + old_environ = dict(os.environ) + if 'http_proxy' in projects.keys(): os.environ['http_proxy'] = projects['http_proxy'] - if 'https_proxy' in projects.keys(): os.environ['https_proxy'] = projects['https_proxy'] @@ -544,6 +545,8 @@ def git_clone_and_install(projects_yaml, core_project): repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, update_requirements=True) + os.environ = old_environ + def _git_validate_projects_yaml(projects, core_project): """ diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index 7c724afc..2abab2ee 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -32,7 +32,9 @@ 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 @@ -44,6 +46,7 @@ from neutron_api_utils import ( determine_packages, determine_ports, do_openstack_upgrade, + git_install, dvr_router_present, l3ha_router_present, register_configs, @@ -114,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()] @@ -143,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() diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index e688c7b0..4edc2785 100644 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -2,6 +2,7 @@ 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 ( @@ -11,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, ) @@ -27,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/' @@ -53,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' ] @@ -115,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)) @@ -282,3 +330,68 @@ def router_feature_present(feature): 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 = [ + '/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 = [ + {'src': src_etc, + 'dest': '/etc/neutron'}, + {'src': os.path.join(src_etc, 'neutron/plugins'), + 'dest': '/etc/neutron/plugins'}, + {'src': os.path.join(src_etc, 'neutron/rootwrap.d'), + 'dest': '/etc/neutron/rootwrap.d'}, + ] + + for c in configs: + if os.path.exists(c['dest']): + shutil.rmtree(c['dest']) + shutil.copytree(c['src'], c['dest']) + + render('git/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('git/upstart/neutron-server.upstart', + '/etc/init/neutron-server.conf', + neutron_api_context, perms=0o644) + + service_restart('neutron-server') diff --git a/templates/git/neutron_sudoers b/templates/git/neutron_sudoers new file mode 100644 index 00000000..d6fec647 --- /dev/null +++ b/templates/git/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/git/upstart/neutron-server.upstart b/templates/git/upstart/neutron-server.upstart new file mode 100644 index 00000000..7211e129 --- /dev/null +++ b/templates/git/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 30e1d99f..ce82b18d 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -1,6 +1,8 @@ #!/usr/bin/python import amulet +import os +import yaml from charmhelpers.contrib.openstack.amulet.deployment import ( OpenStackAmuletDeployment @@ -13,16 +15,18 @@ from charmhelpers.contrib.openstack.amulet.utils import ( ) # Use DEBUG to turn on debug logging -u = OpenStackAmuletUtils(ERROR) +u = OpenStackAmuletUtils(DEBUG) 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 +69,30 @@ 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() + amulet_http_proxy = os.environ.get('AMULET_HTTP_PROXY') + 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': amulet_http_proxy, + 'https_proxy': amulet_http_proxy, + } + 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) @@ -354,6 +377,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', @@ -373,7 +397,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 fef96384..11d49a7c 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -101,7 +101,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, + self.utopic_juno, self.vivid_kilo) = range(10) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, @@ -110,7 +111,9 @@ class OpenStackAmuletDeployment(AmuletDeployment): ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, ('trusty', None): self.trusty_icehouse, ('trusty', 'cloud:trusty-juno'): self.trusty_juno, - ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, + ('utopic', None): self.utopic_juno, + ('vivid', None): self.vivid_kilo} return releases[(self.series, self.openstack)] def _get_openstack_release_string(self): 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..c1483b14 --- /dev/null +++ b/unit_tests/test_actions_git_reinstall.py @@ -0,0 +1,105 @@ +from mock import patch, MagicMock + +with patch('charmhelpers.core.hookenv.config') as config: + config.return_value = 'neutron' + import neutron_api_utils as utils # noqa + +# Need to do some early patching to get the module loaded. +_reg = utils.register_configs +_map = utils.restart_map + +utils.register_configs = MagicMock() +utils.restart_map = MagicMock() + +import git_reinstall + +# Unpatch it now that its loaded. +utils.register_configs = _reg +utils.restart_map = _map + +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') + @patch.object(git_reinstall, 'config_changed') + def test_git_reinstall(self, config_changed, 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.assertTrue(config_changed.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.object(git_reinstall, 'config_changed') + @patch('charmhelpers.contrib.openstack.utils.config') + def test_git_reinstall_not_configured(self, _config, config_changed, + 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.object(git_reinstall, 'config_changed') + @patch('traceback.format_exc') + @patch('charmhelpers.contrib.openstack.utils.config') + def test_git_reinstall_exception(self, _config, format_exc, + config_changed, 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 37, 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") + format_exc.return_value = traceback + + 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_hooks.py b/unit_tests/test_neutron_api_hooks.py index 38893c92..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 @@ -40,6 +41,7 @@ TO_PATCH = [ 'get_l3ha', 'get_l2population', 'get_overlay_network_type', + 'git_install', 'is_relation_made', 'log', 'open_port', @@ -89,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] @@ -106,8 +110,43 @@ 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 @@ -130,6 +169,52 @@ class NeutronAPIHooksTests(CharmTestCase): 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( diff --git a/unit_tests/test_neutron_api_utils.py b/unit_tests/test_neutron_api_utils.py index 9ff7ef45..af0083b5 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,80 @@ 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('/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('os.path.exists') + @patch('shutil.copytree') + @patch('shutil.rmtree') + def test_git_post_install(self, rmtree, copytree, exists, 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'), + call('joined-string', '/etc/neutron/plugins'), + call('joined-string', '/etc/neutron/rootwrap.d'), + ] + copytree.assert_has_calls(expected) + neutron_api_context = { + 'service_description': 'Neutron API server', + 'charm_name': 'neutron-api', + 'process_name': 'neutron-server', + } + expected = [ + call('git/neutron_sudoers', '/etc/sudoers.d/neutron_sudoers', {}, + perms=0o440), + call('git/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)