diff --git a/charmhelpers/contrib/hahelpers/cluster.py b/charmhelpers/contrib/hahelpers/cluster.py index 4a737e24..ba34fba0 100644 --- a/charmhelpers/contrib/hahelpers/cluster.py +++ b/charmhelpers/contrib/hahelpers/cluster.py @@ -25,6 +25,7 @@ Helpers for clustering and determining "cluster leadership" and other clustering-related helpers. """ +import functools import subprocess import os import time @@ -281,6 +282,10 @@ def determine_apache_port(public_port, singlenode_mode=False): return public_port - (i * 10) +determine_apache_port_single = functools.partial( + determine_apache_port, singlenode_mode=True) + + def get_hacluster_config(exclude_keys=None): ''' Obtains all relevant configuration from charm configuration required @@ -404,3 +409,43 @@ def distributed_wait(modulo=None, wait=None, operation_name='operation'): log(msg, DEBUG) status_set('maintenance', msg) time.sleep(calculated_wait) + + +def get_managed_services_and_ports(services, external_ports, + external_services=None, + port_conv_f=determine_apache_port_single): + """Get the services and ports managed by this charm. + + Return only the services and corresponding ports that are managed by this + charm. This excludes haproxy when there is a relation with hacluster. This + is because this charm passes responsability for stopping and starting + haproxy to hacluster. + + Similarly, if a relation with hacluster exists then the ports returned by + this method correspond to those managed by the apache server rather than + haproxy. + + :param services: List of services. + :type services: List[str] + :param external_ports: List of ports managed by external services. + :type external_ports: List[int] + :param external_services: List of services to be removed if ha relation is + present. + :type external_services: List[str] + :param port_conv_f: Function to apply to ports to calculate the ports + managed by services controlled by this charm. + :type port_convert_func: f() + :returns: A tuple containing a list of services first followed by a list of + ports. + :rtype: Tuple[List[str], List[int]] + """ + if external_services is None: + external_services = ['haproxy'] + if relation_ids('ha'): + for svc in external_services: + try: + services.remove(svc) + except ValueError: + pass + external_ports = [port_conv_f(p) for p in external_ports] + return services, external_ports diff --git a/charmhelpers/contrib/hardening/audits/apt.py b/charmhelpers/contrib/hardening/audits/apt.py index 67521e17..cad7bf73 100644 --- a/charmhelpers/contrib/hardening/audits/apt.py +++ b/charmhelpers/contrib/hardening/audits/apt.py @@ -52,7 +52,7 @@ class RestrictedPackages(BaseAudit): def __init__(self, pkgs, **kwargs): super(RestrictedPackages, self).__init__(**kwargs) if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'): - self.pkgs = [pkgs] + self.pkgs = pkgs.split() else: self.pkgs = pkgs @@ -100,4 +100,5 @@ class RestrictedPackages(BaseAudit): apt_purge(pkg.name) def is_virtual_package(self, pkg): - return pkg.has_provides and not pkg.has_versions + return (pkg.get('has_provides', False) and + not pkg.get('has_versions', False)) diff --git a/charmhelpers/contrib/openstack/utils.py b/charmhelpers/contrib/openstack/utils.py index 566404a0..161199c4 100644 --- a/charmhelpers/contrib/openstack/utils.py +++ b/charmhelpers/contrib/openstack/utils.py @@ -44,6 +44,7 @@ from charmhelpers.core.hookenv import ( INFO, ERROR, related_units, + relation_get, relation_ids, relation_set, status_set, @@ -331,6 +332,10 @@ PACKAGE_CODENAMES = { DEFAULT_LOOPBACK_SIZE = '5G' +DB_SERIES_UPGRADING_KEY = 'cluster-series-upgrading' + +DB_MAINTENANCE_KEYS = [DB_SERIES_UPGRADING_KEY] + class CompareOpenStackReleases(BasicStringComparator): """Provide comparisons of OpenStack releases. @@ -1912,3 +1917,33 @@ def set_db_initialised(): """ juju_log('Setting db-initialised to True', 'DEBUG') leader_set({'db-initialised': True}) + + +def is_db_maintenance_mode(relid=None): + """Check relation data from notifications of db in maintenance mode. + + :returns: Whether db has notified it is in maintenance mode. + :rtype: bool + """ + juju_log('Checking for maintenance notifications', 'DEBUG') + if relid: + r_ids = [relid] + else: + r_ids = relation_ids('shared-db') + rids_units = [(r, u) for r in r_ids for u in related_units(r)] + notifications = [] + for r_id, unit in rids_units: + settings = relation_get(unit=unit, rid=r_id) + for key, value in settings.items(): + if value and key in DB_MAINTENANCE_KEYS: + juju_log( + 'Unit: {}, Key: {}, Value: {}'.format(unit, key, value), + 'DEBUG') + try: + notifications.append(bool_from_string(value)) + except ValueError: + juju_log( + 'Could not discern bool from {}'.format(value), + 'WARN') + pass + return True in notifications diff --git a/charmhelpers/fetch/ubuntu_apt_pkg.py b/charmhelpers/fetch/ubuntu_apt_pkg.py index 104f91f1..929a75d7 100644 --- a/charmhelpers/fetch/ubuntu_apt_pkg.py +++ b/charmhelpers/fetch/ubuntu_apt_pkg.py @@ -38,6 +38,7 @@ so with this we get rid of the dependency. import locale import os import subprocess +import sys class _container(dict): @@ -59,6 +60,13 @@ class Cache(object): def __init__(self, progress=None): pass + def __contains__(self, package): + try: + pkg = self.__getitem__(package) + return pkg is not None + except KeyError: + return False + def __getitem__(self, package): """Get information about a package from apt and dpkg databases. @@ -178,6 +186,28 @@ class Cache(object): return pkgs +class Config(_container): + def __init__(self): + super(Config, self).__init__(self._populate()) + + def _populate(self): + cfgs = {} + cmd = ['apt-config', 'dump'] + output = subprocess.check_output(cmd, + stderr=subprocess.STDOUT, + universal_newlines=True) + for line in output.splitlines(): + if not line.startswith("CommandLine"): + k, v = line.split(" ", 1) + cfgs[k] = v.strip(";").strip("\"") + + return cfgs + + +# Backwards compatibility with old apt_pkg module +sys.modules[__name__].config = Config() + + def init(): """Compability shim that does nothing.""" pass diff --git a/hooks/nova_cc_utils.py b/hooks/nova_cc_utils.py index 446b3e58..1578cd93 100644 --- a/hooks/nova_cc_utils.py +++ b/hooks/nova_cc_utils.py @@ -1747,10 +1747,11 @@ def assess_status_func(configs): if cmp_os_release >= 'train': required_interfaces.update(REQUIRED_INTERFACES_TRAIN) required_interfaces.update(get_optional_interfaces()) + _services, _ = ch_cluster.get_managed_services_and_ports(services(), []) return ch_utils.make_assess_status_func( configs, required_interfaces, charm_func=check_optional_relations, - services=services(), ports=None) + services=_services, ports=None) def pause_unit_helper(configs): @@ -1782,8 +1783,9 @@ def _pause_resume_helper(f, configs): """ # TODO(ajkavanagh) - ports= has been left off because of the race hazard # that exists due to service_start() + _services, _ = ch_cluster.get_managed_services_and_ports(services(), []) f(assess_status_func(configs), - services=services(), + services=_services, ports=None) diff --git a/unit_tests/test_nova_cc_utils.py b/unit_tests/test_nova_cc_utils.py index 6aed7d35..cfe0093b 100644 --- a/unit_tests/test_nova_cc_utils.py +++ b/unit_tests/test_nova_cc_utils.py @@ -1254,6 +1254,7 @@ class NovaCCUtilsTests(CharmTestCase): utils.VERSION_PACKAGE ) + @patch.object(utils.ch_cluster, 'get_managed_services_and_ports') @patch.object(utils, 'get_optional_interfaces') @patch.object(utils, 'check_optional_relations') @patch.object(utils, 'REQUIRED_INTERFACES') @@ -1268,8 +1269,10 @@ class NovaCCUtilsTests(CharmTestCase): services, REQUIRED_INTERFACES, check_optional_relations, - get_optional_interfaces): + get_optional_interfaces, + get_managed_services_and_ports): compare_releases.return_value = 'stein' + get_managed_services_and_ports.return_value = (['s1'], ['p1']) services.return_value = 's1' REQUIRED_INTERFACES.copy.return_value = {'int': ['test 1']} get_optional_interfaces.return_value = {'opt': ['test 2']} @@ -1279,7 +1282,7 @@ class NovaCCUtilsTests(CharmTestCase): make_assess_status_func.assert_called_once_with( 'test-config', {'int': ['test 1'], 'opt': ['test 2']}, - charm_func=check_optional_relations, services='s1', + charm_func=check_optional_relations, services=['s1'], ports=None) make_assess_status_func.reset_mock() @@ -1288,7 +1291,7 @@ class NovaCCUtilsTests(CharmTestCase): make_assess_status_func.assert_called_once_with( 'test-config', {'int': ['test 1'], 'placement': ['placement'], 'opt': ['test 2']}, - charm_func=check_optional_relations, services='s1', + charm_func=check_optional_relations, services=['s1'], ports=None) def test_pause_unit_helper(self): @@ -1301,18 +1304,21 @@ class NovaCCUtilsTests(CharmTestCase): prh.assert_called_once_with(utils.ch_utils.resume_unit, 'random-config') + @patch.object(utils.ch_cluster, 'get_managed_services_and_ports') @patch.object(utils, 'services') @patch.object(utils, 'determine_ports') - def test_pause_resume_helper(self, determine_ports, services): + def test_pause_resume_helper(self, determine_ports, services, + get_managed_services_and_ports): f = MagicMock() - services.return_value = 's1' - determine_ports.return_value = 'p1' + get_managed_services_and_ports.return_value = (['s1'], ['p1']) + services.return_value = ['s1'] + determine_ports.return_value = ['p1'] with patch.object(utils, 'assess_status_func') as asf: asf.return_value = 'assessor' utils._pause_resume_helper(f, 'some-config') asf.assert_called_once_with('some-config') # ports=None whilst port checks are disabled. - f.assert_called_once_with('assessor', services='s1', ports=None) + f.assert_called_once_with('assessor', services=['s1'], ports=None) @patch('charmhelpers.fetch.filter_installed_packages') def test_disable_aws_compat_services_uninstalled(