Allow custom theme install

Adds a setting custom-theme which operats similar to ubuntu-theme and
default-theme. The provided resource is placed in the themes folder and
apache is setup to serve static content for the theme. This leaves the
default theme untouched allowing the custom theme to override files
based on the built in horizon theme capabilities. For details on theming
capabilities see:
https://docs.openstack.org/horizon/latest/configuration/themes.html

gnuoy: retry logic for unrelated test updated after a number of CI
failures.

Closes-Bug: #1778284

Change-Id: I91ad19e8aad5c0e0773d42fa4f085cbcecb82458
This commit is contained in:
Chris Sanders 2018-06-22 01:43:23 -05:00 committed by Liam Young
parent 889089153f
commit e5d9c95724
16 changed files with 144 additions and 10 deletions

View File

@ -93,3 +93,22 @@ to also deploy the dashboard with load balancing proxy such as HAProxy:
This option potentially provides better scale-out than using the charm in
conjunction with the hacluster charm.
Custom Theme
============
This charm supports providing a custom theme as documented in the [themes
configuration]. In order to enable this capability the configuration options
'ubuntu-theme' and 'default-theme' must both be turned off and the option
'custom-theme' turned on.
Once the option is enabled a custom theme can be provided via a juju resource.
The resource should be a .tgz file with the contents of your custom theme. If
the file 'local_settings.py' is included it will be sourced.
juju attach-resource openstack-dashboard theme=theme.tgz
Repeating the attach-resource will update the theme and turning off the
custom-theme option will return to the default.
[themes]: https://docs.openstack.org/horizon/latest/configuration/themes.html

View File

@ -175,6 +175,13 @@ options:
.
NOTE: This setting is supported >= OpenStack Liberty and
this setting is mutually exclusive to ubuntu-theme.
custom-theme:
type: boolean
default: False
description: |
Use a custom theme supplied as a resource.
NOTE: This setting is supported >= OpenStack Mitaka and
this setting is mutually exclustive to ubuntu-theme and default-theme.
secret:
type: string
default:

View File

@ -182,6 +182,7 @@ class HorizonContext(OSContextGenerator):
"webroot": config('webroot') or '/',
"ubuntu_theme": bool_from_string(config('ubuntu-theme')),
"default_theme": config('default-theme'),
"custom_theme": config('custom-theme'),
"secret": config('secret') or pwgen(),
'support_profile': config('profile')
if config('profile') in ['cisco'] else None,
@ -210,7 +211,8 @@ class ApacheContext(OSContextGenerator):
'http_port': 70,
'https_port': 433,
'enforce_ssl': False,
'hsts_max_age_seconds': config('hsts-max-age-seconds')
'hsts_max_age_seconds': config('hsts-max-age-seconds'),
"custom_theme": config('custom-theme'),
}
if config('enforce-ssl'):

View File

@ -64,6 +64,7 @@ from horizon_utils import (
restart_on_change,
assess_status,
db_migration,
check_custom_theme,
)
from charmhelpers.contrib.network.ip import (
get_iface_for_address,
@ -110,6 +111,7 @@ def upgrade_charm():
apt_install(filter_installed_packages(determine_packages()), fatal=True)
update_nrpe_config()
CONFIGS.write_all()
check_custom_theme()
@hooks.hook('config-changed')
@ -150,6 +152,7 @@ def config_changed():
save_script_rc(**env_vars)
update_nrpe_config()
CONFIGS.write_all()
check_custom_theme()
open_port(80)
open_port(443)

View File

@ -17,6 +17,7 @@ import horizon_contexts
import os
import subprocess
import time
import tarfile
from collections import OrderedDict
import charmhelpers.contrib.openstack.context as context
@ -36,7 +37,8 @@ from charmhelpers.contrib.openstack.utils import (
)
from charmhelpers.core.hookenv import (
config,
log
log,
resource_get,
)
from charmhelpers.core.host import (
cmp_pkgrevno,
@ -86,6 +88,9 @@ ROUTER_SETTING = ('/usr/share/openstack-dashboard/openstack_dashboard/enabled/'
KEYSTONEV3_POLICY = ('/usr/share/openstack-dashboard/openstack_dashboard/conf/'
'keystonev3_policy.json')
TEMPLATES = 'templates'
CUSTOM_THEME_DIR = ("/usr/share/openstack-dashboard/openstack_dashboard/"
"themes/custom")
LOCAL_DIR = '/usr/share/openstack-dashboard/openstack_dashboard/local/'
CONFIG_FILES = OrderedDict([
(LOCAL_SETTINGS, {
@ -414,3 +419,27 @@ def db_migration():
subcommand = 'syncdb'
cmd = ['/usr/share/openstack-dashboard/manage.py', subcommand, '--noinput']
subprocess.check_call(cmd)
def check_custom_theme():
if not config('custom-theme'):
log('No custom theme configured, exiting')
return
try:
os.mkdir(CUSTOM_THEME_DIR)
except OSError as e:
if e.errno is 17:
pass # already exists
theme_file = resource_get('theme')
log('Retreived resource: {}'.format(theme_file))
if theme_file:
with tarfile.open(theme_file, 'r:gz') as in_file:
in_file.extractall(CUSTOM_THEME_DIR)
custom_settings = '{}/local_settings.py'.format(CUSTOM_THEME_DIR)
if os.path.isfile(custom_settings):
try:
os.symlink(custom_settings, LOCAL_DIR + 'custom_theme.py')
except OSError as e:
if e.errno is 17:
pass # already exists
log('Custom theme updated'.format(theme_file))

View File

@ -38,3 +38,8 @@ requires:
peers:
cluster:
interface: openstack-dashboard-ha
resources:
theme:
type: file
filename: theme.tgz
description: "Custom dashboard theme"

View File

@ -861,6 +861,13 @@ if '{{ default_theme }}' not in [el[0] for el in AVAILABLE_THEMES]:
'themes/{{ default_theme }}'),
]
DEFAULT_THEME = '{{ default_theme }}'
{% elif custom_theme %}
AVAILABLE_THEMES = []
try:
from custom_theme import *
except ImportError:
pass
AVAILABLE_THEMES += [ ('custom', 'custom', 'themes/custom') ]
{% endif %}
WEBROOT = '{{ webroot }}'

View File

@ -0,0 +1,14 @@
WSGIScriptAlias {{ webroot }} /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
WSGIDaemonProcess horizon user=horizon group=horizon processes={{ processes }} threads=10
WSGIProcessGroup horizon
{% if custom_theme %}
Alias /static/custom /usr/share/openstack-dashboard/openstack_dashboard/themes/custom/static/
Alias /static/themes/custom /usr/share/openstack-dashboard/openstack_dashboard/themes/custom/static/
{% endif %}
Alias /static /usr/share/openstack-dashboard/openstack_dashboard/static/
Alias /horizon/static /usr/share/openstack-dashboard/openstack_dashboard/static/
<Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi>
Order allow,deny
Allow from all
</Directory>

View File

@ -900,6 +900,13 @@ if '{{ default_theme }}' not in [el[0] for el in AVAILABLE_THEMES]:
'themes/{{ default_theme }}'),
]
DEFAULT_THEME = '{{ default_theme }}'
{% elif custom_theme %}
AVAILABLE_THEMES = []
try:
from custom_theme import *
except ImportError:
pass
AVAILABLE_THEMES += [ ('custom', 'custom', 'themes/custom') ]
{% endif %}
WEBROOT = '{{ webroot }}'

View File

@ -1,6 +1,9 @@
WSGIScriptAlias {{ webroot }} /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
WSGIDaemonProcess horizon user=horizon group=horizon processes={{ processes }} threads=10
WSGIProcessGroup horizon
{% if custom_theme %}
Alias /static/themes/custom /usr/share/openstack-dashboard/openstack_dashboard/themes/custom/static/
{% endif %}
Alias /static /usr/share/openstack-dashboard/openstack_dashboard/static/
Alias /horizon/static /usr/share/openstack-dashboard/openstack_dashboard/static/
<Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi>

View File

@ -902,6 +902,13 @@ if '{{ default_theme }}' not in [el[0] for el in AVAILABLE_THEMES]:
'themes/{{ default_theme }}'),
]
DEFAULT_THEME = '{{ default_theme }}'
{% elif custom_theme %}
AVAILABLE_THEMES = []
try:
from custom_theme import *
except ImportError:
pass
AVAILABLE_THEMES += [ ('custom', 'custom', 'themes/custom') ]
{% endif %}
WEBROOT = '{{ webroot }}'

View File

@ -1,6 +1,9 @@
WSGIScriptAlias {{ webroot }} /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
WSGIDaemonProcess horizon user=horizon group=horizon processes={{ processes }} threads=10
WSGIProcessGroup horizon
{% if custom_theme %}
Alias /static/themes/custom /usr/share/openstack-dashboard/openstack_dashboard/themes/custom/static/
{% endif %}
Alias /static /var/lib/openstack-dashboard/static/
Alias /horizon/static /var/lib/openstack-dashboard/static/
<Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi>

View File

@ -1,6 +1,9 @@
WSGIScriptAlias {{ webroot }} /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
WSGIDaemonProcess horizon user=horizon group=horizon processes={{ processes }} threads=10
WSGIProcessGroup horizon
{% if custom_theme %}
Alias /static/themes/custom /usr/share/openstack-dashboard/openstack_dashboard/themes/custom/static/
{% endif %}
Alias /static /var/lib/openstack-dashboard/static/
Alias /horizon/static /var/lib/openstack-dashboard/static/
<Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi>

View File

@ -230,7 +230,7 @@ class OpenstackDashboardBasicDeployment(OpenStackAmuletDeployment):
# add retry logic to unwedge the gate. This issue
# should be revisited and root caused properly when time
# allows.
@retry_on_exception(1)
@retry_on_exception(2, base_delay=2)
def do_request():
response = urllib2.urlopen('http://%s/horizon' % (dashboard_ip))
return response.read()

View File

@ -64,7 +64,8 @@ class TestHorizonContexts(CharmTestCase):
self.assertEqual(horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': False,
'hsts_max_age_seconds': 0})
'hsts_max_age_seconds': 0,
'custom_theme': False})
def test_Apachecontext_enforce_ssl(self):
self.test_config.set('enforce-ssl', True)
@ -72,7 +73,8 @@ class TestHorizonContexts(CharmTestCase):
self.assertEquals(horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': True,
'hsts_max_age_seconds': 0})
'hsts_max_age_seconds': 0,
'custom_theme': False})
def test_Apachecontext_enforce_ssl_no_cert(self):
self.test_config.set('enforce-ssl', True)
@ -80,7 +82,8 @@ class TestHorizonContexts(CharmTestCase):
self.assertEquals(horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': False,
'hsts_max_age_seconds': 0})
'hsts_max_age_seconds': 0,
'custom_theme': False})
def test_Apachecontext_hsts_max_age_seconds(self):
self.test_config.set('enforce-ssl', True)
@ -89,7 +92,8 @@ class TestHorizonContexts(CharmTestCase):
self.assertEquals(horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': True,
'hsts_max_age_seconds': 15768000})
'hsts_max_age_seconds': 15768000,
'custom_theme': False})
@patch.object(horizon_contexts, 'get_ca_cert', lambda: None)
@patch('os.chmod')
@ -125,6 +129,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -150,6 +155,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -175,6 +181,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -200,6 +207,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': False,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -226,6 +234,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': False,
'default_theme': 'material',
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -255,6 +264,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -280,6 +290,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'foo', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -305,6 +316,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -335,6 +347,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": True,
@ -360,6 +373,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -385,6 +399,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -411,6 +426,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -437,6 +453,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,
@ -463,6 +480,7 @@ class TestHorizonContexts(CharmTestCase):
'default_role': 'Member', 'webroot': '/horizon',
'ubuntu_theme': True,
'default_theme': None,
'custom_theme': False,
'secret': 'secret',
'support_profile': None,
"neutron_network_dvr": False,

View File

@ -132,11 +132,13 @@ class TestHorizonHooks(CharmTestCase):
)
self.assertTrue(self.apt_install.called)
@patch('horizon_hooks.check_custom_theme')
@patch.object(hooks, 'determine_packages')
@patch.object(utils, 'path_hash')
@patch.object(utils, 'service')
def test_upgrade_charm_hook(self, _service, _hash,
_determine_packages):
_determine_packages,
_custom_theme):
_determine_packages.return_value = []
side_effects = []
[side_effects.append(None) for f in RESTART_MAP.keys()]
@ -155,6 +157,7 @@ class TestHorizonHooks(CharmTestCase):
call('start', 'haproxy'),
]
self.assertEqual(ex, _service.call_args_list)
self.assertTrue(_custom_theme.called)
def test_ha_joined_complete_config(self):
conf = {
@ -258,8 +261,9 @@ class TestHorizonHooks(CharmTestCase):
self.assertTrue(self.update_dns_ha_resource_params.called)
self.relation_set.assert_called_with(**args)
@patch('horizon_hooks.check_custom_theme')
@patch('horizon_hooks.keystone_joined')
def test_config_changed_no_upgrade(self, _joined):
def test_config_changed_no_upgrade(self, _joined, _custom_theme):
def relation_ids_side_effect(rname):
return {
'websso-trusted-dashboard': [
@ -295,13 +299,16 @@ class TestHorizonHooks(CharmTestCase):
self.assertTrue(self.save_script_rc.called)
self.assertTrue(self.CONFIGS.write_all.called)
self.open_port.assert_has_calls([call(80), call(443)])
self.assertTrue(_custom_theme.called)
def test_config_changed_do_upgrade(self):
@patch('horizon_hooks.check_custom_theme')
def test_config_changed_do_upgrade(self, _custom_theme):
self.relation_ids.return_value = []
self.test_config.set('openstack-origin', 'cloud:precise-grizzly')
self.openstack_upgrade_available.return_value = True
self._call_hook('config-changed')
self.assertTrue(self.do_openstack_upgrade.called)
self.assertTrue(_custom_theme.called)
def test_keystone_joined_in_relation(self):
self._call_hook('identity-service-relation-joined')