diff --git a/charmhelpers/contrib/hahelpers/cluster.py b/charmhelpers/contrib/hahelpers/cluster.py index e02350e0..4207e42c 100644 --- a/charmhelpers/contrib/hahelpers/cluster.py +++ b/charmhelpers/contrib/hahelpers/cluster.py @@ -27,6 +27,7 @@ clustering-related helpers. import subprocess import os +import time from socket import gethostname as get_unit_hostname @@ -45,6 +46,9 @@ from charmhelpers.core.hookenv import ( is_leader as juju_is_leader, status_set, ) +from charmhelpers.core.host import ( + modulo_distribution, +) from charmhelpers.core.decorators import ( retry_on_exception, ) @@ -361,3 +365,29 @@ def canonical_url(configs, vip_setting='vip'): else: addr = unit_get('private-address') return '%s://%s' % (scheme, addr) + + +def distributed_wait(modulo=None, wait=None, operation_name='operation'): + ''' Distribute operations by waiting based on modulo_distribution + + If modulo and or wait are not set, check config_get for those values. + + :param modulo: int The modulo number creates the group distribution + :param wait: int The constant time wait value + :param operation_name: string Operation name for status message + i.e. 'restart' + :side effect: Calls config_get() + :side effect: Calls log() + :side effect: Calls status_set() + :side effect: Calls time.sleep() + ''' + if modulo is None: + modulo = config_get('modulo-nodes') + if wait is None: + wait = config_get('known-wait') + calculated_wait = modulo_distribution(modulo=modulo, wait=wait) + msg = "Waiting {} seconds for {} ...".format(calculated_wait, + operation_name) + log(msg, DEBUG) + status_set('maintenance', msg) + time.sleep(calculated_wait) diff --git a/charmhelpers/contrib/hardening/audits/apache.py b/charmhelpers/contrib/hardening/audits/apache.py index d812948a..d32bf44e 100644 --- a/charmhelpers/contrib/hardening/audits/apache.py +++ b/charmhelpers/contrib/hardening/audits/apache.py @@ -70,12 +70,12 @@ class DisabledModuleAudit(BaseAudit): """Returns the modules which are enabled in Apache.""" output = subprocess.check_output(['apache2ctl', '-M']) modules = [] - for line in output.strip().split(): + for line in output.splitlines(): # Each line of the enabled module output looks like: # module_name (static|shared) # Plus a header line at the top of the output which is stripped # out by the regex. - matcher = re.search(r'^ (\S*)', line) + matcher = re.search(r'^ (\S*)_module (\S*)', line) if matcher: modules.append(matcher.group(1)) return modules diff --git a/charmhelpers/contrib/openstack/alternatives.py b/charmhelpers/contrib/openstack/alternatives.py index 1501641e..547de09c 100644 --- a/charmhelpers/contrib/openstack/alternatives.py +++ b/charmhelpers/contrib/openstack/alternatives.py @@ -29,3 +29,16 @@ def install_alternative(name, target, source, priority=50): target, name, source, str(priority) ] subprocess.check_call(cmd) + + +def remove_alternative(name, source): + """Remove an installed alternative configuration file + + :param name: string name of the alternative to remove + :param source: string full path to alternative to remove + """ + cmd = [ + 'update-alternatives', '--remove', + name, source + ] + subprocess.check_call(cmd) diff --git a/charmhelpers/contrib/openstack/files/check_haproxy.sh b/charmhelpers/contrib/openstack/files/check_haproxy.sh index 0df07176..7aab129a 100755 --- a/charmhelpers/contrib/openstack/files/check_haproxy.sh +++ b/charmhelpers/contrib/openstack/files/check_haproxy.sh @@ -9,7 +9,7 @@ CRITICAL=0 NOTACTIVE='' LOGFILE=/var/log/nagios/check_haproxy.log -AUTH=$(grep -r "stats auth" /etc/haproxy | awk 'NR=1{print $4}') +AUTH=$(grep -r "stats auth" /etc/haproxy/haproxy.cfg | awk 'NR=1{print $4}') typeset -i N_INSTANCES=0 for appserver in $(awk '/^\s+server/{print $2}' /etc/haproxy/haproxy.cfg) diff --git a/charmhelpers/core/hookenv.py b/charmhelpers/core/hookenv.py index 12f37b28..622987d1 100644 --- a/charmhelpers/core/hookenv.py +++ b/charmhelpers/core/hookenv.py @@ -218,6 +218,8 @@ def principal_unit(): for rid in relation_ids(reltype): for unit in related_units(rid): md = _metadata_unit(unit) + if not md: + continue subordinate = md.pop('subordinate', None) if not subordinate: return unit @@ -511,7 +513,10 @@ def _metadata_unit(unit): """ basedir = os.sep.join(charm_dir().split(os.sep)[:-2]) unitdir = 'unit-{}'.format(unit.replace(os.sep, '-')) - with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md: + joineddir = os.path.join(basedir, unitdir, 'charm', 'metadata.yaml') + if not os.path.exists(joineddir): + return None + with open(joineddir) as md: return yaml.safe_load(md) @@ -667,6 +672,17 @@ def close_ports(start, end, protocol="TCP"): subprocess.check_call(_args) +def opened_ports(): + """Get the opened ports + + *Note that this will only show ports opened in a previous hook* + + :returns: Opened ports as a list of strings: ``['8080/tcp', '8081-8083/tcp']`` + """ + _args = ['opened-ports', '--format=json'] + return json.loads(subprocess.check_output(_args).decode('UTF-8')) + + @cached def unit_get(attribute): """Get the unit ID for the remote unit""" diff --git a/charmhelpers/core/host.py b/charmhelpers/core/host.py index 5656e2f5..2ac94e04 100644 --- a/charmhelpers/core/host.py +++ b/charmhelpers/core/host.py @@ -34,7 +34,7 @@ import six from contextlib import contextmanager from collections import OrderedDict -from .hookenv import log, DEBUG +from .hookenv import log, DEBUG, local_unit from .fstab import Fstab from charmhelpers.osplatform import get_platform @@ -946,3 +946,31 @@ def updatedb(updatedb_text, new_path): lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths)) output = "\n".join(lines) return output + + +def modulo_distribution(modulo=3, wait=30): + """ Modulo distribution + + This helper uses the unit number, a modulo value and a constant wait time + to produce a calculated wait time distribution. This is useful in large + scale deployments to distribute load during an expensive operation such as + service restarts. + + If you have 1000 nodes that need to restart 100 at a time 1 minute at a + time: + + time.wait(modulo_distribution(modulo=100, wait=60)) + restart() + + If you need restarts to happen serially set modulo to the exact number of + nodes and set a high constant wait time: + + time.wait(modulo_distribution(modulo=10, wait=120)) + restart() + + @param modulo: int The modulo number creates the group distribution + @param wait: int The constant time wait value + @return: int Calculated time to wait for unit operation + """ + unit_number = int(local_unit().split('/')[1]) + return (unit_number % modulo) * wait diff --git a/tests/charmhelpers/core/hookenv.py b/tests/charmhelpers/core/hookenv.py index 12f37b28..622987d1 100644 --- a/tests/charmhelpers/core/hookenv.py +++ b/tests/charmhelpers/core/hookenv.py @@ -218,6 +218,8 @@ def principal_unit(): for rid in relation_ids(reltype): for unit in related_units(rid): md = _metadata_unit(unit) + if not md: + continue subordinate = md.pop('subordinate', None) if not subordinate: return unit @@ -511,7 +513,10 @@ def _metadata_unit(unit): """ basedir = os.sep.join(charm_dir().split(os.sep)[:-2]) unitdir = 'unit-{}'.format(unit.replace(os.sep, '-')) - with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md: + joineddir = os.path.join(basedir, unitdir, 'charm', 'metadata.yaml') + if not os.path.exists(joineddir): + return None + with open(joineddir) as md: return yaml.safe_load(md) @@ -667,6 +672,17 @@ def close_ports(start, end, protocol="TCP"): subprocess.check_call(_args) +def opened_ports(): + """Get the opened ports + + *Note that this will only show ports opened in a previous hook* + + :returns: Opened ports as a list of strings: ``['8080/tcp', '8081-8083/tcp']`` + """ + _args = ['opened-ports', '--format=json'] + return json.loads(subprocess.check_output(_args).decode('UTF-8')) + + @cached def unit_get(attribute): """Get the unit ID for the remote unit""" diff --git a/tests/charmhelpers/core/host.py b/tests/charmhelpers/core/host.py index 5656e2f5..2ac94e04 100644 --- a/tests/charmhelpers/core/host.py +++ b/tests/charmhelpers/core/host.py @@ -34,7 +34,7 @@ import six from contextlib import contextmanager from collections import OrderedDict -from .hookenv import log, DEBUG +from .hookenv import log, DEBUG, local_unit from .fstab import Fstab from charmhelpers.osplatform import get_platform @@ -946,3 +946,31 @@ def updatedb(updatedb_text, new_path): lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths)) output = "\n".join(lines) return output + + +def modulo_distribution(modulo=3, wait=30): + """ Modulo distribution + + This helper uses the unit number, a modulo value and a constant wait time + to produce a calculated wait time distribution. This is useful in large + scale deployments to distribute load during an expensive operation such as + service restarts. + + If you have 1000 nodes that need to restart 100 at a time 1 minute at a + time: + + time.wait(modulo_distribution(modulo=100, wait=60)) + restart() + + If you need restarts to happen serially set modulo to the exact number of + nodes and set a high constant wait time: + + time.wait(modulo_distribution(modulo=10, wait=120)) + restart() + + @param modulo: int The modulo number creates the group distribution + @param wait: int The constant time wait value + @return: int Calculated time to wait for unit operation + """ + unit_number = int(local_unit().split('/')[1]) + return (unit_number % modulo) * wait