diff --git a/.project b/.project new file mode 100644 index 00000000..5a0efccc --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + openstack-dashboard + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 00000000..ee88aa82 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,9 @@ + + +python 2.7 +Default + +/openstack-dashboard/hooks +/openstack-dashboard/unit_tests + + diff --git a/charm-helpers-sync.yaml b/charm-helpers-sync.yaml index 32f99bf5..ffc1471b 100644 --- a/charm-helpers-sync.yaml +++ b/charm-helpers-sync.yaml @@ -6,4 +6,5 @@ include: - contrib.openstack - contrib.storage.linux - contrib.hahelpers + - contrib.storage - payload.execd diff --git a/config.yaml b/config.yaml index 045713ad..83d55772 100644 --- a/config.yaml +++ b/config.yaml @@ -26,6 +26,12 @@ options: description: | Default role for Horizon operations that will be created in Keystone upon introduction of an identity-service relation. + use-syslog: + type: boolean + default: False + description: | + By default, all services will log into their corresponding log files. + Setting this to True will force all services to log to the syslog. vip: type: string description: "Virtual IP to use to front openstack dashboard ha configuration" @@ -80,4 +86,4 @@ options: description: Use Ubuntu theme for the dashboard. secret: type: string - descriptions: Secret for Horizon to use when securing internal data; set this when using multiple dashboard units. + description: Secret for Horizon to use when securing internal data; set this when using multiple dashboard units. diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py index 074855f4..bf832f7d 100644 --- a/hooks/charmhelpers/contrib/hahelpers/cluster.py +++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py @@ -126,17 +126,17 @@ def determine_api_port(public_port): return public_port - (i * 10) -def determine_haproxy_port(public_port): +def determine_apache_port(public_port): ''' - Description: Determine correct proxy listening port based on public IP + - existence of HTTPS reverse proxy. + Description: Determine correct apache listening port based on public IP + + state of the cluster. public_port: int: standard public port for given service returns: int: the correct listening port for the HAProxy service ''' i = 0 - if https(): + if len(peer_units()) > 0 or is_clustered(): i += 1 return public_port - (i * 10) diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 8a982ffa..81ac12b6 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -23,15 +23,12 @@ from charmhelpers.core.hookenv import ( unit_get, unit_private_ip, ERROR, - WARNING, ) from charmhelpers.contrib.hahelpers.cluster import ( + determine_apache_port, determine_api_port, - determine_haproxy_port, https, - is_clustered, - peer_units, ) from charmhelpers.contrib.hahelpers.apache import ( @@ -68,6 +65,43 @@ def context_complete(ctxt): return True +def config_flags_parser(config_flags): + if config_flags.find('==') >= 0: + log("config_flags is not in expected format (key=value)", + level=ERROR) + raise OSContextError + # strip the following from each value. + post_strippers = ' ,' + # we strip any leading/trailing '=' or ' ' from the string then + # split on '='. + split = config_flags.strip(' =').split('=') + limit = len(split) + flags = {} + for i in xrange(0, limit - 1): + current = split[i] + next = split[i + 1] + vindex = next.rfind(',') + if (i == limit - 2) or (vindex < 0): + value = next + else: + value = next[:vindex] + + if i == 0: + key = current + else: + # if this not the first entry, expect an embedded key. + index = current.rfind(',') + if index < 0: + log("invalid config value(s) at index %s" % (i), + level=ERROR) + raise OSContextError + key = current[index + 1:] + + # Add to collection. + flags[key.strip(post_strippers)] = value.rstrip(post_strippers) + return flags + + class OSContextGenerator(object): interfaces = [] @@ -182,10 +216,17 @@ class AMQPContext(OSContextGenerator): # Sufficient information found = break out! break # Used for active/active rabbitmq >= grizzly - ctxt['rabbitmq_hosts'] = [] - for unit in related_units(rid): - ctxt['rabbitmq_hosts'].append(relation_get('private-address', - rid=rid, unit=unit)) + if ('clustered' not in ctxt or relation_get('ha-vip-only') == 'True') and \ + len(related_units(rid)) > 1: + if relation_get('ha_queues'): + ctxt['rabbitmq_ha_queues'] = relation_get('ha_queues') + else: + ctxt['rabbitmq_ha_queues'] = False + rabbitmq_hosts = [] + for unit in related_units(rid): + rabbitmq_hosts.append(relation_get('private-address', + rid=rid, unit=unit)) + ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts) if not context_complete(ctxt): return {} else: @@ -209,11 +250,13 @@ class CephContext(OSContextGenerator): unit=unit)) auth = relation_get('auth', rid=rid, unit=unit) key = relation_get('key', rid=rid, unit=unit) + use_syslog = str(config('use-syslog')).lower() ctxt = { 'mon_hosts': ' '.join(mon_hosts), 'auth': auth, 'key': key, + 'use_syslog': use_syslog } if not os.path.isdir('/etc/ceph'): @@ -286,6 +329,7 @@ class ImageServiceContext(OSContextGenerator): class ApacheSSLContext(OSContextGenerator): + """ Generates a context for an apache vhost configuration that configures HTTPS reverse proxying for one or many endpoints. Generated context @@ -341,11 +385,9 @@ class ApacheSSLContext(OSContextGenerator): 'private_address': unit_get('private-address'), 'endpoints': [] } - for ext_port in self.external_ports: - if peer_units() or is_clustered(): - int_port = determine_haproxy_port(ext_port) - else: - int_port = determine_api_port(ext_port) + for api_port in self.external_ports: + ext_port = determine_apache_port(api_port) + int_port = determine_api_port(api_port) portmap = (int(ext_port), int(int_port)) ctxt['endpoints'].append(portmap) return ctxt @@ -428,33 +470,37 @@ class NeutronContext(object): elif self.plugin == 'nvp': ctxt.update(self.nvp_ctxt()) + alchemy_flags = config('neutron-alchemy-flags') + if alchemy_flags: + flags = config_flags_parser(alchemy_flags) + ctxt['neutron_alchemy_flags'] = flags + self._save_flag_file() return ctxt class OSConfigFlagContext(OSContextGenerator): - ''' - Responsible adding user-defined config-flags in charm config to a - to a template context. - ''' + + """ + Responsible for adding user-defined config-flags in charm config to a + template context. + + NOTE: the value of config-flags may be a comma-separated list of + key=value pairs and some Openstack config files support + comma-separated lists as values. + """ + def __call__(self): config_flags = config('config-flags') - if not config_flags or config_flags in ['None', '']: + if not config_flags: return {} - config_flags = config_flags.split(',') - flags = {} - for flag in config_flags: - if '=' not in flag: - log('Improperly formatted config-flag, expected k=v ' - 'got %s' % flag, level=WARNING) - continue - k, v = flag.split('=') - flags[k.strip()] = v - ctxt = {'user_config_flags': flags} - return ctxt + + flags = config_flags_parser(config_flags) + return {'user_config_flags': flags} class SubordinateConfigContext(OSContextGenerator): + """ Responsible for inspecting relations to subordinates that may be exporting required config via a json blob. @@ -495,6 +541,7 @@ class SubordinateConfigContext(OSContextGenerator): } """ + def __init__(self, service, config_file, interface): """ :param service : Service name key to query in any subordinate @@ -539,3 +586,12 @@ class SubordinateConfigContext(OSContextGenerator): ctxt['sections'] = {} return ctxt + + +class SyslogContext(OSContextGenerator): + + def __call__(self): + ctxt = { + 'use_syslog': config('use-syslog') + } + return ctxt diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 43c7df3e..56d04245 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -202,7 +202,7 @@ def os_release(package, base='essex'): def import_key(keyid): - cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \ + cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \ "--recv-keys %s" % keyid try: subprocess.check_call(cmd.split(' ')) @@ -415,7 +415,7 @@ def get_host_ip(hostname): return ns_query(hostname) -def get_hostname(address): +def get_hostname(address, fqdn=True): """ Resolves hostname for given IP, or returns the input if it is already a hostname. @@ -434,7 +434,11 @@ def get_hostname(address): if not result: return None - # strip trailing . - if result.endswith('.'): - return result[:-1] - return result + if fqdn: + # strip trailing . + if result.endswith('.'): + return result[:-1] + else: + return result + else: + return result.split('.')[0] diff --git a/hooks/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py index 69b879ca..12417410 100644 --- a/hooks/charmhelpers/contrib/storage/linux/ceph.py +++ b/hooks/charmhelpers/contrib/storage/linux/ceph.py @@ -49,6 +49,9 @@ CEPH_CONF = """[global] auth supported = {auth} keyring = {keyring} mon host = {mon_hosts} + log to syslog = {use_syslog} + err to syslog = {use_syslog} + clog to syslog = {use_syslog} """ @@ -194,7 +197,7 @@ def get_ceph_nodes(): return hosts -def configure(service, key, auth): +def configure(service, key, auth, use_syslog): ''' Perform basic configuration of Ceph ''' create_keyring(service, key) create_key_file(service, key) @@ -202,7 +205,8 @@ def configure(service, key, auth): with open('/etc/ceph/ceph.conf', 'w') as ceph_conf: ceph_conf.write(CEPH_CONF.format(auth=auth, keyring=_keyring_path(service), - mon_hosts=",".join(map(str, hosts)))) + mon_hosts=",".join(map(str, hosts)), + use_syslog=use_syslog)) modprobe('rbd') diff --git a/hooks/charmhelpers/contrib/storage/linux/utils.py b/hooks/charmhelpers/contrib/storage/linux/utils.py index 5b9b6d47..c40218f0 100644 --- a/hooks/charmhelpers/contrib/storage/linux/utils.py +++ b/hooks/charmhelpers/contrib/storage/linux/utils.py @@ -22,4 +22,4 @@ def zap_disk(block_device): :param block_device: str: Full path of block device to clean. ''' - check_call(['sgdisk', '--zap-all', block_device]) + check_call(['sgdisk', '--zap-all', '--mbrtogpt', block_device]) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index bb196dfa..505c202d 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -8,6 +8,7 @@ import os import json import yaml import subprocess +import sys import UserDict from subprocess import CalledProcessError @@ -149,6 +150,11 @@ def service_name(): return local_unit().split('/')[0] +def hook_name(): + """The name of the currently executing hook""" + return os.path.basename(sys.argv[0]) + + @cached def config(scope=None): """Juju charm configuration""" diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py index c8c81b28..cfd26847 100644 --- a/hooks/charmhelpers/core/host.py +++ b/hooks/charmhelpers/core/host.py @@ -194,7 +194,7 @@ def file_hash(path): return None -def restart_on_change(restart_map): +def restart_on_change(restart_map, stopstart=False): """Restart services based on configuration files changing This function is used a decorator, for example @@ -219,8 +219,14 @@ def restart_on_change(restart_map): for path in restart_map: if checksums[path] != file_hash(path): restarts += restart_map[path] - for service_name in list(OrderedDict.fromkeys(restarts)): - service('restart', service_name) + services_list = list(OrderedDict.fromkeys(restarts)) + if not stopstart: + for service_name in services_list: + service('restart', service_name) + else: + for action in ['stop', 'start']: + for service_name in services_list: + service(action, service_name) return wrapped_f return wrap diff --git a/hooks/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py index 1f4f6315..07bb707d 100644 --- a/hooks/charmhelpers/fetch/__init__.py +++ b/hooks/charmhelpers/fetch/__init__.py @@ -136,7 +136,7 @@ def apt_hold(packages, fatal=False): def add_source(source, key=None): if (source.startswith('ppa:') or - source.startswith('http:') or + source.startswith('http') or source.startswith('deb ') or source.startswith('cloud-archive:')): subprocess.check_call(['add-apt-repository', '--yes', source]) @@ -156,7 +156,9 @@ def add_source(source, key=None): with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: apt.write(PROPOSED_POCKET.format(release)) if key: - subprocess.check_call(['apt-key', 'import', key]) + subprocess.check_call(['apt-key', 'adv', '--keyserver', + 'keyserver.ubuntu.com', '--recv', + key]) class SourceConfigError(Exception): diff --git a/hooks/horizon_hooks.py b/hooks/horizon_hooks.py index 22a3a1dc..ac4e3b19 100755 --- a/hooks/horizon_hooks.py +++ b/hooks/horizon_hooks.py @@ -34,6 +34,7 @@ from horizon_utils import ( from charmhelpers.contrib.hahelpers.apache import install_ca_cert from charmhelpers.contrib.hahelpers.cluster import get_hacluster_config from charmhelpers.payload.execd import execd_preinstall +from base64 import b64decode hooks = Hooks() CONFIGS = register_configs() @@ -95,7 +96,7 @@ def keystone_joined(rel_id=None): def keystone_changed(): CONFIGS.write(LOCAL_SETTINGS) if relation_get('ca_cert'): - install_ca_cert(relation_get('ca_cert')) + install_ca_cert(b64decode(relation_get('ca_cert'))) @hooks.hook('cluster-relation-departed', diff --git a/hooks/horizon_utils.py b/hooks/horizon_utils.py index 62c1e1b7..806b64fc 100644 --- a/hooks/horizon_utils.py +++ b/hooks/horizon_utils.py @@ -1,6 +1,7 @@ # vim: set ts=4:et import horizon_contexts import charmhelpers.contrib.openstack.templating as templating +import charmhelpers.contrib.openstack.context as context import subprocess import os from collections import OrderedDict @@ -38,15 +39,18 @@ TEMPLATES = 'templates' CONFIG_FILES = OrderedDict([ (LOCAL_SETTINGS, { 'hook_contexts': [horizon_contexts.HorizonContext(), - horizon_contexts.IdentityServiceContext()], + horizon_contexts.IdentityServiceContext(), + context.SyslogContext()], 'services': ['apache2'] }), (APACHE_CONF, { - 'hook_contexts': [horizon_contexts.HorizonContext()], + 'hook_contexts': [horizon_contexts.HorizonContext(), + context.SyslogContext()], 'services': ['apache2'], }), (APACHE_24_CONF, { - 'hook_contexts': [horizon_contexts.HorizonContext()], + 'hook_contexts': [horizon_contexts.HorizonContext(), + context.SyslogContext()], 'services': ['apache2'], }), (APACHE_SSL, { diff --git a/revision b/revision index e85087af..f5c89552 100644 --- a/revision +++ b/revision @@ -1 +1 @@ -31 +32 diff --git a/templates/grizzly/local_settings.py b/templates/grizzly/local_settings.py index 87c79228..af4bf198 100644 --- a/templates/grizzly/local_settings.py +++ b/templates/grizzly/local_settings.py @@ -1,8 +1,10 @@ import os from django.utils.translation import ugettext_lazy as _ - from openstack_dashboard import exceptions +{% if use_syslog %} +from logging.handlers import SysLogHandler +{% endif %} DEBUG = {{ debug }} TEMPLATE_DEBUG = DEBUG @@ -176,6 +178,12 @@ LOGGING = { 'level': 'DEBUG', 'class': 'django.utils.log.NullHandler', }, + {% if use_syslog %} + 'syslog': { + 'level': 'INFO', + 'class': 'logging.handlers.SysLogHandler', + }, + {% endif %} 'console': { # Set the level to "DEBUG" for verbose output logging. 'level': 'INFO', @@ -194,27 +202,59 @@ LOGGING = { 'propagate': False, }, 'horizon': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'openstack_dashboard': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} + 'propagate': False, + }, + 'openstack_auth': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} + 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'novaclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'keystoneclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'glanceclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'nose.plugins.manager': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, } } diff --git a/templates/havana/local_settings.py b/templates/havana/local_settings.py index a04f153c..0f9e0c95 100644 --- a/templates/havana/local_settings.py +++ b/templates/havana/local_settings.py @@ -1,6 +1,9 @@ import os from django.utils.translation import ugettext_lazy as _ +{% if use_syslog %} +from logging.handlers import SysLogHandler +{% endif %} from openstack_dashboard import exceptions @@ -248,6 +251,12 @@ LOGGING = { 'level': 'INFO', 'class': 'logging.StreamHandler', }, + {% if use_syslog %} + 'syslog': { + 'level': 'INFO', + 'class': 'logging.handlers.SysLogHandler', + } + {% endif %} }, 'loggers': { # Logging from django.db.backends is VERY verbose, send to null @@ -261,35 +270,75 @@ LOGGING = { 'propagate': False, }, 'horizon': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'openstack_dashboard': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} + 'propagate': False, + }, + 'openstack_auth': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} + 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'novaclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'cinderclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'keystoneclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'glanceclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'heatclient': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, }, 'nose.plugins.manager': { + {% if use_syslog %} + 'handlers': ['syslog'], + {% else %} 'handlers': ['console'], + {% endif %} 'propagate': False, } } diff --git a/unit_tests/test_horizon_contexts.py b/unit_tests/test_horizon_contexts.py index 69487b63..451eb329 100644 --- a/unit_tests/test_horizon_contexts.py +++ b/unit_tests/test_horizon_contexts.py @@ -40,6 +40,7 @@ def patch_open(): class TestHorizonContexts(CharmTestCase): + def setUp(self): super(TestHorizonContexts, self).setUp(horizon_contexts, TO_PATCH) self.config.side_effect = self.test_config.get @@ -67,7 +68,7 @@ class TestHorizonContexts(CharmTestCase): call('key') ]) # Security check on key permissions - _chmod.assert_called_with('/etc/ssl/private/dashboard.key', 0600) + _chmod.assert_called_with('/etc/ssl/private/dashboard.key', 0o600) def test_ApacheSSLContext_disabled(self): self.get_cert.return_value = (None, None) @@ -78,42 +79,48 @@ class TestHorizonContexts(CharmTestCase): self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': True, 'debug': False, 'default_role': 'Member', 'webroot': '/horizon', - 'ubuntu_theme': True, 'secret': 'secret'}) + 'ubuntu_theme': True, + 'secret': 'secret'}) def test_HorizonContext_debug(self): self.test_config.set('debug', 'yes') self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': True, 'debug': True, 'default_role': 'Member', 'webroot': '/horizon', - 'ubuntu_theme': True, 'secret': 'secret'}) + 'ubuntu_theme': True, + 'secret': 'secret'}) def test_HorizonContext_theme(self): self.test_config.set('ubuntu-theme', False) self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': True, 'debug': False, 'default_role': 'Member', 'webroot': '/horizon', - 'ubuntu_theme': False, 'secret': 'secret'}) + 'ubuntu_theme': False, + 'secret': 'secret'}) def test_HorizonContext_compression(self): self.test_config.set('offline-compression', 'no') self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': False, 'debug': False, 'default_role': 'Member', 'webroot': '/horizon', - 'ubuntu_theme': True, 'secret': 'secret'}) + 'ubuntu_theme': True, + 'secret': 'secret'}) def test_HorizonContext_role(self): self.test_config.set('default-role', 'foo') self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': True, 'debug': False, 'default_role': 'foo', 'webroot': '/horizon', - 'ubuntu_theme': True, 'secret': 'secret'}) + 'ubuntu_theme': True, + 'secret': 'secret'}) def test_HorizonContext_webroot(self): self.test_config.set('webroot', '/') self.assertEquals(horizon_contexts.HorizonContext()(), {'compress_offline': True, 'debug': False, 'default_role': 'Member', 'webroot': '/', - 'ubuntu_theme': True, 'secret': 'secret'}) + 'ubuntu_theme': True, + 'secret': 'secret'}) def test_IdentityServiceContext_not_related(self): self.relation_ids.return_value = [] diff --git a/unit_tests/test_horizon_hooks.py b/unit_tests/test_horizon_hooks.py index b830cd23..da52641e 100644 --- a/unit_tests/test_horizon_hooks.py +++ b/unit_tests/test_horizon_hooks.py @@ -27,7 +27,12 @@ TO_PATCH = [ 'install_ca_cert', 'unit_get', 'log', - 'execd_preinstall'] + 'execd_preinstall', + 'b64decode'] + + +def passthrough(value): + return value class TestHorizonHooks(CharmTestCase): @@ -35,6 +40,7 @@ class TestHorizonHooks(CharmTestCase): def setUp(self): super(TestHorizonHooks, self).setUp(hooks, TO_PATCH) self.config.side_effect = self.test_config.get + self.b64decode.side_effect = passthrough def _call_hook(self, hookname): hooks.hooks.execute([ diff --git a/unit_tests/test_horizon_utils.py b/unit_tests/test_horizon_utils.py index fb0ec540..bad87eae 100644 --- a/unit_tests/test_horizon_utils.py +++ b/unit_tests/test_horizon_utils.py @@ -20,6 +20,7 @@ TO_PATCH = [ class TestHorizonUtils(CharmTestCase): + def setUp(self): super(TestHorizonUtils, self).setUp(horizon_utils, TO_PATCH) diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py index fd7fe233..e04a9dc8 100644 --- a/unit_tests/test_utils.py +++ b/unit_tests/test_utils.py @@ -44,6 +44,7 @@ def get_default_config(): class CharmTestCase(unittest.TestCase): + def setUp(self, obj, patches): super(CharmTestCase, self).setUp() self.patches = patches @@ -64,6 +65,7 @@ class CharmTestCase(unittest.TestCase): class TestConfig(object): + def __init__(self): self.config = get_default_config() @@ -83,6 +85,7 @@ class TestConfig(object): class TestRelation(object): + def __init__(self, relation_data={}): self.relation_data = relation_data