This commit is contained in:
Ryan Beisner
2015-09-02 02:20:28 +00:00
90 changed files with 306 additions and 48 deletions

View File

@@ -4,3 +4,4 @@ exclude_lines =
if __name__ == .__main__.:
include=
hooks/keystone_*
actions/actions.py

View File

@@ -1,2 +1,13 @@
git-reinstall:
description: Reinstall keystone from the openstack-origin-git repositories.
pause:
description: |
Pause keystone services.
If the keystone deployment is clustered using the hacluster charm, the
corresponding hacluster unit on the node must first be paused as well.
Not doing so may lead to an interruption of service.
resume:
description: |
Resume keystone services.
If the keystone deployment is clustered using the hacluster charm, the
corresponding hacluster unit on the node must be resumed as well.

0
actions/__init__.py Normal file
View File

56
actions/actions.py Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/python
import sys
import os
from charmhelpers.core.host import service_pause, service_resume
from charmhelpers.core.hookenv import action_fail, status_set
from hooks.keystone_utils import services
def pause(args):
"""Pause all the Keystone services.
@raises Exception if any services fail to stop
"""
for service in services():
stopped = service_pause(service)
if not stopped:
raise Exception("{} didn't stop cleanly.".format(service))
status_set(
"maintenance", "Paused. Use 'resume' action to resume normal service.")
def resume(args):
"""Resume all the Keystone services.
@raises Exception if any services fail to start
"""
for service in services():
started = service_resume(service)
if not started:
raise Exception("{} didn't start cleanly.".format(service))
status_set("active", "")
# A dictionary of all the defined actions to callables (which take
# parsed arguments).
ACTIONS = {"pause": pause, "resume": resume}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
return "Action %s undefined" % action_name
else:
try:
action(args)
except Exception as e:
action_fail(str(e))
if __name__ == "__main__":
sys.exit(main(sys.argv))

1
actions/charmhelpers Symbolic link
View File

@@ -0,0 +1 @@
../charmhelpers

View File

@@ -1,8 +1,6 @@
#!/usr/bin/python
import sys
import traceback
sys.path.append('hooks/')
import traceback
from charmhelpers.contrib.openstack.utils import (
git_install_requested,
@@ -14,11 +12,11 @@ from charmhelpers.core.hookenv import (
config,
)
from keystone_utils import (
from hooks.keystone_utils import (
git_install,
)
from keystone_hooks import (
from hooks.keystone_hooks import (
config_changed,
)

1
actions/hooks Symbolic link
View File

@@ -0,0 +1 @@
../hooks

1
actions/pause Symbolic link
View File

@@ -0,0 +1 @@
actions.py

1
actions/resume Symbolic link
View File

@@ -0,0 +1 @@
actions.py

View File

@@ -1,5 +1,5 @@
branch: lp:charm-helpers
destination: hooks/charmhelpers
destination: charmhelpers
include:
- core
- cli

0
hooks/__init__.py Normal file
View File

1
hooks/charmhelpers Symbolic link
View File

@@ -0,0 +1 @@
../charmhelpers

View File

@@ -14,6 +14,7 @@ import time
import urlparse
import uuid
from itertools import chain
from base64 import b64encode
from collections import OrderedDict
from copy import deepcopy
@@ -150,11 +151,6 @@ GIT_PACKAGE_BLACKLIST = [
'keystone',
]
API_PORTS = {
'keystone-admin': config('admin-port'),
'keystone-public': config('service-port')
}
KEYSTONE_CONF = "/etc/keystone/keystone.conf"
KEYSTONE_LOGGER_CONF = "/etc/keystone/logging.conf"
KEYSTONE_CONF_DIR = os.path.dirname(KEYSTONE_CONF)
@@ -178,7 +174,6 @@ SSH_USER = 'juju_keystone'
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
SSL_SYNC_SEMAPHORE = threading.Semaphore()
SSL_DIRS = [SSL_DIR, APACHE_SSL_DIR, CA_CERT_PATH]
BASE_RESOURCE_MAP = OrderedDict([
(KEYSTONE_CONF, {
'services': BASE_SERVICES,
@@ -321,11 +316,8 @@ def restart_map():
def services():
"""Returns a list of services associate with this charm"""
_services = []
for v in restart_map().values():
_services = _services + v
return list(set(_services))
"""Returns a list of (unique) services associated with this charm"""
return list(set(chain(*restart_map().values())))
def determine_ports():
@@ -335,23 +327,20 @@ def determine_ports():
def api_port(service):
return API_PORTS[service]
return {
'keystone-admin': config('admin-port'),
'keystone-public': config('service-port')
}[service]
def determine_packages():
# currently all packages match service names
packages = [] + BASE_PACKAGES
for k, v in resource_map().iteritems():
packages.extend(v['services'])
packages = set(services()).union(BASE_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)
packages |= set(BASE_GIT_PACKAGES)
packages -= set(GIT_PACKAGE_BLACKLIST)
return list(set(packages))
return sorted(packages)
def save_script_rc():

View File

@@ -38,6 +38,21 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
self._deploy()
self._initialize_tests()
def _assert_services(self, should_run):
u.get_unit_process_ids(
{self.keystone_sentry: ("keystone-all", "apache2", "haproxy")},
expect_success=should_run)
def get_service_overrides(self, unit):
"""
Return a dict mapping service names to a boolean indicating whether
an override file exists for that service.
"""
init_contents = unit.directory_contents("/etc/init/")
return {
service: "{}.override".format(service) in init_contents["files"]
for service in ("keystone", "apache2", "haproxy")}
def _add_services(self):
"""Add services
@@ -462,4 +477,23 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
amulet.raise_status(amulet.FAIL, msg=msg)
self.d.configure(juju_service, set_default)
u.log.debug('OK')
def test_901_pause_resume(self):
"""Test pause and resume actions."""
unit_name = "keystone/0"
unit = self.d.sentry.unit[unit_name]
self._assert_services(should_run=True)
action_id = u.run_action(unit, "pause")
assert u.wait_on_action(action_id), "Pause action failed."
self._assert_services(should_run=False)
assert all(self.get_service_overrides(unit).itervalues()), \
"Not all override files were created."
action_id = u.run_action(unit, "resume")
assert u.wait_on_action(action_id), "Resume action failed"
assert not any(self.get_service_overrides(unit).itervalues()), \
"Not all override files were removed."
self._assert_services(should_run=True)

150
unit_tests/test_actions.py Normal file
View File

@@ -0,0 +1,150 @@
import mock
from test_utils import CharmTestCase
import actions.actions
class PauseTestCase(CharmTestCase):
def setUp(self):
super(PauseTestCase, self).setUp(
actions.actions, ["service_pause", "status_set"])
def test_pauses_services(self):
"""Pause action pauses all Keystone services."""
pause_calls = []
def fake_service_pause(svc):
pause_calls.append(svc)
return True
self.service_pause.side_effect = fake_service_pause
actions.actions.pause([])
self.assertEqual(pause_calls, ['haproxy', 'keystone', 'apache2'])
def test_bails_out_early_on_error(self):
"""Pause action fails early if there are errors stopping a service."""
pause_calls = []
def maybe_kill(svc):
if svc == "keystone":
return False
else:
pause_calls.append(svc)
return True
self.service_pause.side_effect = maybe_kill
self.assertRaisesRegexp(
Exception, "keystone didn't stop cleanly.",
actions.actions.pause, [])
self.assertEqual(pause_calls, ['haproxy'])
def test_status_mode(self):
"""Pause action sets the status to maintenance."""
status_calls = []
self.status_set.side_effect = lambda state, msg: status_calls.append(
state)
actions.actions.pause([])
self.assertEqual(status_calls, ["maintenance"])
def test_status_message(self):
"""Pause action sets a status message reflecting that it's paused."""
status_calls = []
self.status_set.side_effect = lambda state, msg: status_calls.append(
msg)
actions.actions.pause([])
self.assertEqual(
status_calls, ["Paused. "
"Use 'resume' action to resume normal service."])
class ResumeTestCase(CharmTestCase):
def setUp(self):
super(ResumeTestCase, self).setUp(
actions.actions, ["service_resume", "status_set"])
def test_resumes_services(self):
"""Resume action resumes all Keystone services."""
resume_calls = []
def fake_service_resume(svc):
resume_calls.append(svc)
return True
self.service_resume.side_effect = fake_service_resume
actions.actions.resume([])
self.assertEqual(resume_calls, ['haproxy', 'keystone', 'apache2'])
def test_bails_out_early_on_error(self):
"""Resume action fails early if there are errors starting a service."""
resume_calls = []
def maybe_kill(svc):
if svc == "keystone":
return False
else:
resume_calls.append(svc)
return True
self.service_resume.side_effect = maybe_kill
self.assertRaisesRegexp(
Exception, "keystone didn't start cleanly.",
actions.actions.resume, [])
self.assertEqual(resume_calls, ['haproxy'])
def test_status_mode(self):
"""Resume action sets the status to maintenance."""
status_calls = []
self.status_set.side_effect = lambda state, msg: status_calls.append(
state)
actions.actions.resume([])
self.assertEqual(status_calls, ["active"])
def test_status_message(self):
"""Resume action sets an empty status message."""
status_calls = []
self.status_set.side_effect = lambda state, msg: status_calls.append(
msg)
actions.actions.resume([])
self.assertEqual(status_calls, [""])
class MainTestCase(CharmTestCase):
def setUp(self):
super(MainTestCase, self).setUp(actions.actions, ["action_fail"])
def test_invokes_action(self):
dummy_calls = []
def dummy_action(args):
dummy_calls.append(True)
with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}):
actions.actions.main(["foo"])
self.assertEqual(dummy_calls, [True])
def test_unknown_action(self):
"""Unknown actions aren't a traceback."""
exit_string = actions.actions.main(["foo"])
self.assertEqual("Action foo undefined", exit_string)
def test_failing_action(self):
"""Actions which traceback trigger action_fail() calls."""
dummy_calls = []
self.action_fail.side_effect = dummy_calls.append
def dummy_action(args):
raise ValueError("uh oh")
with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}):
actions.actions.main(["foo"])
self.assertEqual(dummy_calls, ["uh oh"])

View File

@@ -4,8 +4,8 @@ with patch('charmhelpers.core.hookenv.config') as config:
config.return_value = 'keystone'
import keystone_utils as utils # noqa
with patch('keystone_utils.register_configs') as register_configs:
import git_reinstall
with patch('keystone_utils.register_configs') as register_configs:
import git_reinstall
from test_utils import (
CharmTestCase
@@ -36,8 +36,10 @@ class TestKeystoneActions(CharmTestCase):
@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):
@patch('charmhelpers.contrib.openstack.utils.config')
def test_git_reinstall(self, config, config_changed, git_install,
action_fail, action_set):
config.return_value = openstack_origin_git
self.test_config.set('openstack-origin-git', openstack_origin_git)
git_reinstall.git_reinstall()

View File

@@ -1,3 +1,5 @@
import os
import keystone_context as context
from mock import patch, MagicMock
@@ -45,6 +47,7 @@ class TestKeystoneContexts(CharmTestCase):
self.assertTrue(mock_ensure_permissions.called)
self.assertFalse(mock_get_ca.called)
@patch('keystone_utils.determine_ports')
@patch('keystone_utils.is_ssl_cert_master')
@patch('keystone_utils.is_ssl_enabled')
@patch('charmhelpers.contrib.openstack.context.config')
@@ -60,7 +63,8 @@ class TestKeystoneContexts(CharmTestCase):
mock_is_clustered,
mock_config,
mock_is_ssl_enabled,
mock_is_ssl_cert_master):
mock_is_ssl_cert_master,
mock_determine_ports):
mock_is_ssl_enabled.return_value = True
mock_is_ssl_cert_master.return_value = True
mock_https.return_value = True
@@ -69,6 +73,7 @@ class TestKeystoneContexts(CharmTestCase):
mock_determine_apache_port.return_value = '34'
mock_is_clustered.return_value = False
mock_config.return_value = None
mock_determine_ports.return_value = ['12']
ctxt = context.ApacheSSLContext()
ctxt.enable_modules = MagicMock()
@@ -83,6 +88,7 @@ class TestKeystoneContexts(CharmTestCase):
self.assertTrue(mock_https.called)
mock_unit_get.assert_called_with('private-address')
@patch('keystone_utils.api_port')
@patch('charmhelpers.contrib.openstack.context.get_netmask_for_address')
@patch('charmhelpers.contrib.openstack.context.get_address_in_network')
@patch('charmhelpers.contrib.openstack.context.config')
@@ -95,7 +101,10 @@ class TestKeystoneContexts(CharmTestCase):
def test_haproxy_context_service_enabled(
self, mock_open, mock_log, mock_relation_get, mock_related_units,
mock_unit_get, mock_relation_ids, mock_config,
mock_get_address_in_network, mock_get_netmask_for_address):
mock_get_address_in_network, mock_get_netmask_for_address,
mock_api_port):
os.environ['JUJU_UNIT_NAME'] = 'keystone'
mock_relation_ids.return_value = ['identity-service:0', ]
mock_unit_get.return_value = '1.2.3.4'
mock_relation_get.return_value = '10.0.0.0'
@@ -104,19 +113,20 @@ class TestKeystoneContexts(CharmTestCase):
mock_get_address_in_network.return_value = None
mock_get_netmask_for_address.return_value = '255.255.255.0'
self.determine_apache_port.return_value = '34'
mock_api_port.return_value = '12'
ctxt = context.HAProxyContext()
self.maxDiff = None
self.assertEquals(
ctxt(),
{'listen_ports': {'admin_port': 'keystone',
'public_port': 'keystone'},
{'listen_ports': {'admin_port': '12',
'public_port': '12'},
'local_host': '127.0.0.1',
'haproxy_host': '0.0.0.0',
'stat_port': ':8888',
'service_ports': {'admin-port': ['keystone', '34'],
'public-port': ['keystone', '34']},
'service_ports': {'admin-port': ['12', '34'],
'public-port': ['12', '34']},
'default_backend': '1.2.3.4',
'frontends': {'1.2.3.4': {
'network': '1.2.3.4/255.255.255.0',

View File

@@ -92,9 +92,9 @@ class KeystoneRelationTests(CharmTestCase):
self.configure_installation_source.assert_called_with(repo)
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(
['haproxy', 'unison', 'python-keystoneclient',
'uuid', 'python-mysqldb', 'openssl', 'apache2',
'pwgen', 'python-six', 'keystone', 'python-psycopg2'], fatal=True)
['apache2', 'haproxy', 'keystone', 'openssl', 'pwgen',
'python-keystoneclient', 'python-mysqldb', 'python-psycopg2',
'python-six', 'unison', 'uuid'], fatal=True)
self.git_install.assert_called_with(None)
@patch.object(utils, 'git_install_requested')
@@ -120,12 +120,12 @@ class KeystoneRelationTests(CharmTestCase):
self.configure_installation_source.assert_called_with(repo)
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(
['haproxy', 'unison', 'python-setuptools', 'python-keystoneclient',
'uuid', 'python-mysqldb', 'libmysqlclient-dev', 'libssl-dev',
'openssl', 'libffi-dev', 'apache2', 'python-pip', 'pwgen',
'python-six', 'libxslt1-dev', 'python-psycopg2', 'libyaml-dev',
'zlib1g-dev', 'python-dev', 'libxml2-dev'],
fatal=True)
['apache2', 'haproxy', 'libffi-dev', 'libmysqlclient-dev',
'libssl-dev', 'libxml2-dev', 'libxslt1-dev', 'libyaml-dev',
'openssl', 'pwgen', 'python-dev', 'python-keystoneclient',
'python-mysqldb', 'python-pip', 'python-psycopg2',
'python-setuptools', 'python-six', 'unison', 'uuid',
'zlib1g-dev'], fatal=True)
self.git_install.assert_called_with(projects_yaml)
mod_ch_openstack_utils = 'charmhelpers.contrib.openstack.utils'

View File

@@ -281,11 +281,13 @@ class TestKeystoneUtils(CharmTestCase):
self.relation_set.assert_called_with(relation_id=relation_id,
**filtered)
@patch('charmhelpers.contrib.openstack.ip.config')
@patch.object(utils, 'ensure_valid_service')
@patch.object(utils, 'add_endpoint')
@patch.object(manager, 'KeystoneManager')
def test_add_service_to_keystone_nosubset(
self, KeystoneManager, add_endpoint, ensure_valid_service):
self, KeystoneManager, add_endpoint, ensure_valid_service,
ip_config):
relation_id = 'identity-service:0'
remote_unit = 'unit/0'