Merge from trunk

This commit is contained in:
Brad Crittenden 2015-09-28 15:15:37 -04:00
commit 1d70417506
20 changed files with 319 additions and 14 deletions

View File

@ -1,2 +1,4 @@
git-reinstall:
description: Reinstall openstack-dashboard from the openstack-origin-git repositories.
openstack-upgrade:
description: Perform openstack upgrades. Config option action-managed-upgrade must be set to True.

1
actions/openstack-upgrade Symbolic link
View File

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

34
actions/openstack_upgrade.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/python
import sys
sys.path.append('hooks/')
from charmhelpers.contrib.openstack.utils import (
do_action_openstack_upgrade,
)
from horizon_utils import (
do_openstack_upgrade,
)
from horizon_hooks import (
config_changed,
CONFIGS,
)
def openstack_upgrade():
"""Upgrade packages to config-set Openstack version.
If the charm was installed from source we cannot upgrade it.
For backwards compatibility a config flag must be set for this
code to run, otherwise a full service level upgrade will fire
on config-changed."""
if do_action_openstack_upgrade('openstack-dashboard',
do_openstack_upgrade,
CONFIGS):
config_changed()
if __name__ == '__main__':
openstack_upgrade()

View File

@ -153,6 +153,14 @@ options:
In order for this charm to function correctly, the privacy extension
must be disabled and a non-temporary address must be
configured/available on your network interface.
endpoint-type:
type: string
default:
description: |
Specifies the endpoint types to use for endpoints in the Keystone
service catalog. Valid values are 'publicURL', 'internalURL',
and 'adminURL'. Both the primary and secondary endpoint types can
be specified by providing multiple comma delimited values.
nagios_context:
default: "juju"
type: string
@ -175,4 +183,13 @@ options:
description: |
A comma-separated list of nagios servicegroups. If left empty, the
nagios_context will be used as the servicegroup.
action-managed-upgrade:
type: boolean
default: False
description: |
If True enables openstack upgrades for this charm via juju actions.
You will still need to set openstack-origin to the new repository but
instead of an upgrade running automatically across all units, it will
wait for you to execute the openstack-upgrade action for this charm on
each unit. If False it will revert to existing behavior of upgrading
all units on config change.

View File

@ -25,6 +25,7 @@ import sys
import re
import six
import traceback
import yaml
from charmhelpers.contrib.network import ip
@ -34,6 +35,8 @@ from charmhelpers.core import (
)
from charmhelpers.core.hookenv import (
action_fail,
action_set,
config,
log as juju_log,
charm_dir,
@ -114,6 +117,7 @@ SWIFT_CODENAMES = OrderedDict([
('2.2.1', 'kilo'),
('2.2.2', 'kilo'),
('2.3.0', 'liberty'),
('2.4.0', 'liberty'),
])
# >= Liberty version->codename mapping
@ -748,3 +752,47 @@ def git_yaml_value(projects_yaml, key):
return projects[key]
return None
def do_action_openstack_upgrade(package, upgrade_callback, configs):
"""Perform action-managed OpenStack upgrade.
Upgrades packages to the configured openstack-origin version and sets
the corresponding action status as a result.
If the charm was installed from source we cannot upgrade it.
For backwards compatibility a config flag (action-managed-upgrade) must
be set for this code to run, otherwise a full service level upgrade will
fire on config-changed.
@param package: package name for determining if upgrade available
@param upgrade_callback: function callback to charm's upgrade function
@param configs: templating object derived from OSConfigRenderer class
@return: True if upgrade successful; False if upgrade failed or skipped
"""
ret = False
if git_install_requested():
action_set({'outcome': 'installed from source, skipped upgrade.'})
else:
if openstack_upgrade_available(package):
if config('action-managed-upgrade'):
juju_log('Upgrading OpenStack release')
try:
upgrade_callback(configs=configs)
action_set({'outcome': 'success, upgrade completed.'})
ret = True
except:
action_set({'outcome': 'upgrade failed, see traceback.'})
action_set({'traceback': traceback.format_exc()})
action_fail('do_openstack_upgrade resulted in an '
'unexpected error')
else:
action_set({'outcome': 'action-managed-upgrade config is '
'False, skipped upgrade.'})
else:
action_set({'outcome': 'no upgrade available.'})
return ret

View File

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

View File

@ -6,7 +6,8 @@ from charmhelpers.core.hookenv import (
relation_get,
local_unit,
unit_get,
log
log,
ERROR,
)
from charmhelpers.contrib.openstack.context import (
OSContextGenerator,
@ -28,6 +29,12 @@ from charmhelpers.core.host import pwgen
from base64 import b64decode
import os
VALID_ENDPOINT_TYPES = {
'PUBLICURL': 'publicURL',
'INTERNALURL': 'internalURL',
'ADMINURL': 'adminURL',
}
class HorizonHAProxyContext(HAProxyContext):
def __call__(self):
@ -69,6 +76,22 @@ class HorizonHAProxyContext(HAProxyContext):
class IdentityServiceContext(OSContextGenerator):
interfaces = ['identity-service']
def normalize(self, endpoint_type):
"""Normalizes the endpoint type values.
:param endpoint_type (string): the endpoint type to normalize.
:raises: Exception if the endpoint type is not valid.
:return (string): the normalized form of the endpoint type.
"""
normalized_form = VALID_ENDPOINT_TYPES.get(endpoint_type.upper(), None)
if not normalized_form:
msg = ('Endpoint type specified %s is not a valid'
' endpoint type' % endpoint_type)
log(msg, ERROR)
raise Exception(msg)
return normalized_form
def __call__(self):
log('Generating template context for identity-service')
ctxt = {}
@ -106,6 +129,21 @@ class IdentityServiceContext(OSContextGenerator):
avail_regions = map(lambda r: {'endpoint': r[0], 'title': r[1]},
regions)
ctxt['regions'] = sorted(avail_regions)
# Allow the endpoint types to be specified via a config parameter.
# The config parameter accepts either:
# 1. a single endpoint type to be specified, in which case the
# primary endpoint is configured
# 2. a list of endpoint types, in which case the primary endpoint
# is taken as the first entry and the secondary endpoint is
# taken as the second entry. All subsequent entries are ignored.
ep_types = config('endpoint-type')
if ep_types:
ep_types = [self.normalize(e) for e in ep_types.split(',')]
ctxt['primary_endpoint'] = ep_types[0]
if len(ep_types) > 1:
ctxt['secondary_endpoint'] = ep_types[1]
return ctxt
@ -172,3 +210,28 @@ class RouterSettingContext(OSContextGenerator):
'disable_router': False if config('profile') in ['cisco'] else True
}
return ctxt
class LocalSettingsContext(OSContextGenerator):
def __call__(self):
''' Additional config stanzas to be appended to local_settings.py '''
relations = []
for rid in relation_ids("plugin"):
try:
unit = related_units(rid)[0]
except IndexError:
pass
else:
rdata = relation_get(unit=unit, rid=rid)
if set(('local-settings', 'priority')) <= set(rdata.keys()):
relations.append((unit, rdata))
ctxt = {
'settings': [
'# {0}\n{1}'.format(u, rd['local-settings'])
for u, rd in sorted(relations,
key=lambda r: r[1]['priority'])]
}
return ctxt

View File

@ -58,7 +58,7 @@ hooks = Hooks()
CONFIGS = register_configs()
@hooks.hook('install')
@hooks.hook('install.real')
def install():
execd_preinstall()
configure_installation_source(config('openstack-origin'))
@ -106,7 +106,7 @@ def config_changed():
if git_install_requested():
if config_value_changed('openstack-origin-git'):
git_install(config('openstack-origin-git'))
else:
elif not config('action-managed-upgrade'):
if openstack_upgrade_available('openstack-dashboard'):
do_openstack_upgrade(configs=CONFIGS)
@ -247,16 +247,23 @@ def update_nrpe_config():
@hooks.hook('dashboard-plugin-relation-joined')
def update_dashboard_plugin(rel_id=None):
def plugin_relation_joined(rel_id=None):
if git_install_requested():
bin_path = git_pip_venv_dir(config('openstack-origin-git'))
else:
bin_path = '/usr/bin'
relation_set(relation_id=rel_id,
relation_set(release=os_release("openstack-dashboard"),
relation_id=rel_id,
bin_path=bin_path,
openstack_dir=INSTALL_DIR)
@hooks.hook('dashboard-plugin-relation-changed')
@restart_on_change(restart_map())
def update_plugin_config():
CONFIGS.write(LOCAL_SETTINGS)
def main():
try:
hooks.execute(sys.argv)

View File

@ -101,7 +101,8 @@ CONFIG_FILES = OrderedDict([
(LOCAL_SETTINGS, {
'hook_contexts': [horizon_contexts.HorizonContext(),
horizon_contexts.IdentityServiceContext(),
context.SyslogContext()],
context.SyslogContext(),
horizon_contexts.LocalSettingsContext()],
'services': ['apache2']
}),
(APACHE_CONF, {
@ -262,12 +263,12 @@ def setup_ipv6():
raise Exception("IPv6 is not supported in the charms for Ubuntu "
"versions less than Trusty 14.04")
# NOTE(xianghui): Need to install haproxy(1.5.3) from trusty-backports
# to support ipv6 address, so check is required to make sure not
# breaking other versions, IPv6 only support for >= Trusty
if ubuntu_rel == 'trusty':
add_source('deb http://archive.ubuntu.com/ubuntu trusty-backports'
' main')
# Need haproxy >= 1.5.3 for ipv6 so for Trusty if we are <= Kilo we need to
# use trusty-backports otherwise we can use the UCA.
os_pkg = 'openstack-dashboard'
if ubuntu_rel == 'trusty' and os_release(os_pkg) < 'liberty':
add_source('deb http://archive.ubuntu.com/ubuntu trusty-backports '
'main')
apt_update()
apt_install('haproxy/trusty-backports', fatal=True)

View File

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

20
hooks/install Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
# Wrapper to deal with newer Ubuntu versions that don't have py2 installed
# by default.
declare -a DEPS=('apt' 'netaddr' 'netifaces' 'pip' 'yaml')
check_and_install() {
pkg="${1}-${2}"
if ! dpkg -s ${pkg} 2>&1 > /dev/null; then
apt-get -y install ${pkg}
fi
}
PYTHON="python"
for dep in ${DEPS[@]}; do
check_and_install ${PYTHON} ${dep}
done
exec ./hooks/install.real

1
hooks/install.real Symbolic link
View File

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

View File

@ -21,6 +21,9 @@ requires:
ha:
interface: hacluster
scope: container
dashboard-plugin:
interface: dashboard-plugin
scope: container
peers:
cluster:
interface: openstack-dashboard-ha

View File

@ -118,3 +118,5 @@ LOGGING = {
}
}
}
{{ settings|join('\n\n') }}

View File

@ -167,3 +167,5 @@ LOGIN_REDIRECT_URL='{{ webroot }}'
# offline compression by default. To enable online compression, install
# the node-less package and enable the following option.
COMPRESS_OFFLINE = {{ compress_offline }}
{{ settings|join('\n\n') }}

View File

@ -263,3 +263,5 @@ LOGGING = {
}
}
}
{{ settings|join('\n\n') }}

View File

@ -483,3 +483,5 @@ COMPRESS_OFFLINE = {{ compress_offline }}
# installations should have this set accordingly. For more information
# see https://docs.djangoproject.com/en/dev/ref/settings/.
ALLOWED_HOSTS = '*'
{{ settings|join('\n\n') }}

View File

@ -212,6 +212,9 @@ IMAGE_CUSTOM_PROPERTY_TITLES = {
# in the Keystone service catalog. Use this setting when Horizon is running
# external to the OpenStack environment. The default is 'publicURL'.
#OPENSTACK_ENDPOINT_TYPE = "publicURL"
{% if primary_endpoint -%}
OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}
{% endif -%}
# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
# case that OPENSTACK_ENDPOINT_TYPE is not present in the endpoints
@ -219,6 +222,9 @@ IMAGE_CUSTOM_PROPERTY_TITLES = {
# external to the OpenStack environment. The default is None. This
# value should differ from OPENSTACK_ENDPOINT_TYPE if used.
#SECONDARY_ENDPOINT_TYPE = "publicURL"
{% if secondary_endpoint -%}
SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}
{% endif -%}
# The number of objects (Swift containers/objects or images) to display
# on a single page before providing a paging element (a "more" link)
@ -514,3 +520,5 @@ COMPRESS_OFFLINE = {{ compress_offline }}
# installations should have this set accordingly. For more information
# see https://docs.djangoproject.com/en/dev/ref/settings/.
ALLOWED_HOSTS = '*'
{{ settings|join('\n\n') }}

View File

@ -250,6 +250,9 @@ IMAGE_RESERVED_CUSTOM_PROPERTIES = []
# in the Keystone service catalog. Use this setting when Horizon is running
# external to the OpenStack environment. The default is 'publicURL'.
#OPENSTACK_ENDPOINT_TYPE = "publicURL"
{% if primary_endpoint -%}
OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}
{% endif -%}
# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
# case that OPENSTACK_ENDPOINT_TYPE is not present in the endpoints
@ -257,6 +260,9 @@ IMAGE_RESERVED_CUSTOM_PROPERTIES = []
# external to the OpenStack environment. The default is None. This
# value should differ from OPENSTACK_ENDPOINT_TYPE if used.
#SECONDARY_ENDPOINT_TYPE = "publicURL"
{% if secondary_endpoint -%}
SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}
{% endif -%}
# The number of objects (Swift containers/objects or images) to display
# on a single page before providing a paging element (a "more" link)
@ -619,3 +625,5 @@ LOGIN_REDIRECT_URL='{{ webroot }}'
# installations should have this set accordingly. For more information
# see https://docs.djangoproject.com/en/dev/ref/settings/.
ALLOWED_HOSTS = '*'
{{ settings|join('\n\n') }}

View File

@ -0,0 +1,53 @@
from mock import patch
import os
os.environ['JUJU_UNIT_NAME'] = 'openstack-dashboard'
with patch('horizon_utils.register_configs') as register_configs:
import openstack_upgrade
from test_utils import (
CharmTestCase
)
TO_PATCH = [
'do_openstack_upgrade',
'config_changed',
]
class TestHorizonUpgradeActions(CharmTestCase):
def setUp(self):
super(TestHorizonUpgradeActions, self).setUp(openstack_upgrade,
TO_PATCH)
@patch('charmhelpers.contrib.openstack.utils.config')
@patch('charmhelpers.contrib.openstack.utils.action_set')
@patch('charmhelpers.contrib.openstack.utils.git_install_requested')
@patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
def test_openstack_upgrade_true(self, upgrade_avail, git_requested,
action_set, config):
git_requested.return_value = False
upgrade_avail.return_value = True
config.return_value = True
openstack_upgrade.openstack_upgrade()
self.assertTrue(self.do_openstack_upgrade.called)
self.assertTrue(self.config_changed.called)
@patch('charmhelpers.contrib.openstack.utils.config')
@patch('charmhelpers.contrib.openstack.utils.action_set')
@patch('charmhelpers.contrib.openstack.utils.git_install_requested')
@patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
def test_openstack_upgrade_false(self, upgrade_avail, git_requested,
action_set, config):
git_requested.return_value = False
upgrade_avail.return_value = True
config.return_value = False
openstack_upgrade.openstack_upgrade()
self.assertFalse(self.do_openstack_upgrade.called)
self.assertFalse(self.config_changed.called)

View File

@ -212,6 +212,22 @@ class TestHorizonContexts(CharmTestCase):
{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionTwo'}]})
def test_IdentityServiceContext_endpoint_type(self):
self.test_config.set('endpoint-type', 'internalURL')
self.assertEqual(horizon_contexts.IdentityServiceContext()(),
{'primary_endpoint': 'internalURL'})
def test_IdentityServiceContext_multi_endpoint_types(self):
self.test_config.set('endpoint-type', 'internalURL,publicURL')
self.assertEqual(horizon_contexts.IdentityServiceContext()(),
{'primary_endpoint': 'internalURL',
'secondary_endpoint': 'publicURL'})
def test_IdentityServiceContext_invalid_endpoint_type(self):
self.test_config.set('endpoint-type', 'this_is_bad')
with self.assertRaises(Exception):
horizon_contexts.IdentityServiceContext()()
def test_HorizonHAProxyContext_no_cluster(self):
self.relation_ids.return_value = []
self.local_unit.return_value = 'openstack-dashboard/0'
@ -251,3 +267,18 @@ class TestHorizonContexts(CharmTestCase):
self.test_config.set('profile', None)
self.assertEquals(horizon_contexts.RouterSettingContext()(),
{'disable_router': True, })
def test_LocalSettingsContext(self):
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': 99,
'local-settings': 'FOO = True'},
{'priority': 60,
'local-settings': 'BAR = False'}]
self.assertEquals(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})