diff --git a/charmhelpers/contrib/openstack/amulet/deployment.py b/charmhelpers/contrib/openstack/amulet/deployment.py index 1c96752a..5b7e3cfb 100644 --- a/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/charmhelpers/contrib/openstack/amulet/deployment.py @@ -168,7 +168,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): 'nrpe', 'openvswitch-odl', 'neutron-api-odl', 'odl-controller', 'cinder-backup', 'nexentaedge-data', 'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw', - 'cinder-nexentaedge', 'nexentaedge-mgmt'])) + 'cinder-nexentaedge', 'nexentaedge-mgmt', + 'ceilometer-agent'])) if self.openstack: for svc in services: diff --git a/charmhelpers/contrib/openstack/ha/utils.py b/charmhelpers/contrib/openstack/ha/utils.py index add8eb9a..cdf4b4c9 100644 --- a/charmhelpers/contrib/openstack/ha/utils.py +++ b/charmhelpers/contrib/openstack/ha/utils.py @@ -23,6 +23,7 @@ Helpers for high availability. """ +import hashlib import json import re @@ -35,7 +36,6 @@ from charmhelpers.core.hookenv import ( config, status_set, DEBUG, - WARNING, ) from charmhelpers.core.host import ( @@ -124,13 +124,29 @@ def expect_ha(): return len(ha_related_units) > 0 or config('vip') or config('dns-ha') -def generate_ha_relation_data(service): +def generate_ha_relation_data(service, extra_settings=None): """ Generate relation data for ha relation Based on configuration options and unit interfaces, generate a json encoded dict of relation data items for the hacluster relation, providing configuration for DNS HA or VIP's + haproxy clone sets. + Example of supplying additional settings:: + + COLO_CONSOLEAUTH = 'inf: res_nova_consoleauth grp_nova_vips' + AGENT_CONSOLEAUTH = 'ocf:openstack:nova-consoleauth' + AGENT_CA_PARAMS = 'op monitor interval="5s"' + + ha_console_settings = { + 'colocations': {'vip_consoleauth': COLO_CONSOLEAUTH}, + 'init_services': {'res_nova_consoleauth': 'nova-consoleauth'}, + 'resources': {'res_nova_consoleauth': AGENT_CONSOLEAUTH}, + 'resource_params': {'res_nova_consoleauth': AGENT_CA_PARAMS}) + generate_ha_relation_data('nova', extra_settings=ha_console_settings) + + + @param service: Name of the service being configured + @param extra_settings: Dict of additional resource data @returns dict: json encoded data for use with relation_set """ _haproxy_res = 'res_{}_haproxy'.format(service) @@ -149,6 +165,13 @@ def generate_ha_relation_data(service): }, } + if extra_settings: + for k, v in extra_settings.items(): + if _relation_data.get(k): + _relation_data[k].update(v) + else: + _relation_data[k] = v + if config('dns-ha'): update_hacluster_dns_ha(service, _relation_data) else: @@ -232,40 +255,75 @@ def update_hacluster_vip(service, relation_data): """ cluster_config = get_hacluster_config() vip_group = [] + vips_to_delete = [] for vip in cluster_config['vip'].split(): if is_ipv6(vip): - res_neutron_vip = 'ocf:heartbeat:IPv6addr' + res_vip = 'ocf:heartbeat:IPv6addr' vip_params = 'ipv6addr' else: - res_neutron_vip = 'ocf:heartbeat:IPaddr2' + res_vip = 'ocf:heartbeat:IPaddr2' vip_params = 'ip' - iface = (get_iface_for_address(vip) or - config('vip_iface')) - netmask = (get_netmask_for_address(vip) or - config('vip_cidr')) + iface = get_iface_for_address(vip) + netmask = get_netmask_for_address(vip) + + fallback_params = False + if iface is None: + iface = config('vip_iface') + fallback_params = True + if netmask is None: + netmask = config('vip_cidr') + fallback_params = True if iface is not None: + # NOTE(jamespage): Delete old VIP resources + # Old style naming encoding iface in name + # does not work well in environments where + # interface/subnet wiring is not consistent vip_key = 'res_{}_{}_vip'.format(service, iface) - if vip_key in vip_group: - if vip not in relation_data['resource_params'][vip_key]: - vip_key = '{}_{}'.format(vip_key, vip_params) - else: - log("Resource '%s' (vip='%s') already exists in " - "vip group - skipping" % (vip_key, vip), WARNING) - continue + if vip_key in vips_to_delete: + vip_key = '{}_{}'.format(vip_key, vip_params) + vips_to_delete.append(vip_key) + + vip_key = 'res_{}_{}_vip'.format( + service, + hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7]) + + relation_data['resources'][vip_key] = res_vip + # NOTE(jamespage): + # Use option provided vip params if these where used + # instead of auto-detected values + if fallback_params: + relation_data['resource_params'][vip_key] = ( + 'params {ip}="{vip}" cidr_netmask="{netmask}" ' + 'nic="{iface}"'.format(ip=vip_params, + vip=vip, + iface=iface, + netmask=netmask) + ) + else: + # NOTE(jamespage): + # let heartbeat figure out which interface and + # netmask to configure, which works nicely + # when network interface naming is not + # consistent across units. + relation_data['resource_params'][vip_key] = ( + 'params {ip}="{vip}"'.format(ip=vip_params, + vip=vip)) - relation_data['resources'][vip_key] = res_neutron_vip - relation_data['resource_params'][vip_key] = ( - 'params {ip}="{vip}" cidr_netmask="{netmask}" ' - 'nic="{iface}"'.format(ip=vip_params, - vip=vip, - iface=iface, - netmask=netmask) - ) vip_group.append(vip_key) + if vips_to_delete: + try: + relation_data['delete_resources'].extend(vips_to_delete) + except KeyError: + relation_data['delete_resources'] = vips_to_delete + if len(vip_group) >= 1: - relation_data['groups'] = { - 'grp_{}_vips'.format(service): ' '.join(vip_group) - } + key = 'grp_{}_vips'.format(service) + try: + relation_data['groups'][key] = ' '.join(vip_group) + except KeyError: + relation_data['groups'] = { + key: ' '.join(vip_group) + } diff --git a/charmhelpers/contrib/openstack/utils.py b/charmhelpers/contrib/openstack/utils.py index 29cad083..59312fcf 100644 --- a/charmhelpers/contrib/openstack/utils.py +++ b/charmhelpers/contrib/openstack/utils.py @@ -73,6 +73,8 @@ from charmhelpers.core.host import ( service_running, service_pause, service_resume, + service_stop, + service_start, restart_on_change_helper, ) from charmhelpers.fetch import ( @@ -299,7 +301,7 @@ def get_os_codename_install_source(src): rel = '' if src is None: return rel - if src in ['distro', 'distro-proposed']: + if src in ['distro', 'distro-proposed', 'proposed']: try: rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel] except KeyError: @@ -1303,6 +1305,65 @@ def is_unit_paused_set(): return False +def manage_payload_services(action, services=None, charm_func=None): + """Run an action against all services. + + An optional charm_func() can be called. It should raise an Exception to + indicate that the function failed. If it was succesfull it should return + None or an optional message. + + The signature for charm_func is: + charm_func() -> message: str + + charm_func() is executed after any services are stopped, if supplied. + + The services object can either be: + - None : no services were passed (an empty dict is returned) + - a list of strings + - A dictionary (optionally OrderedDict) {service_name: {'service': ..}} + - An array of [{'service': service_name, ...}, ...] + + :param action: Action to run: pause, resume, start or stop. + :type action: str + :param services: See above + :type services: See above + :param charm_func: function to run for custom charm pausing. + :type charm_func: f() + :returns: Status boolean and list of messages + :rtype: (bool, []) + :raises: RuntimeError + """ + actions = { + 'pause': service_pause, + 'resume': service_resume, + 'start': service_start, + 'stop': service_stop} + action = action.lower() + if action not in actions.keys(): + raise RuntimeError( + "action: {} must be one of: {}".format(action, + ', '.join(actions.keys()))) + services = _extract_services_list_helper(services) + messages = [] + success = True + if services: + for service in services.keys(): + rc = actions[action](service) + if not rc: + success = False + messages.append("{} didn't {} cleanly.".format(service, + action)) + if charm_func: + try: + message = charm_func() + if message: + messages.append(message) + except Exception as e: + success = False + messages.append(str(e)) + return success, messages + + def pause_unit(assess_status_func, services=None, ports=None, charm_func=None): """Pause a unit by stopping the services and setting 'unit-paused' @@ -1333,20 +1394,10 @@ def pause_unit(assess_status_func, services=None, ports=None, @returns None @raises Exception(message) on an error for action_fail(). """ - services = _extract_services_list_helper(services) - messages = [] - if services: - for service in services.keys(): - stopped = service_pause(service) - if not stopped: - messages.append("{} didn't stop cleanly.".format(service)) - if charm_func: - try: - message = charm_func() - if message: - messages.append(message) - except Exception as e: - message.append(str(e)) + _, messages = manage_payload_services( + 'pause', + services=services, + charm_func=charm_func) set_unit_paused() if assess_status_func: message = assess_status_func() @@ -1385,20 +1436,10 @@ def resume_unit(assess_status_func, services=None, ports=None, @returns None @raises Exception(message) on an error for action_fail(). """ - services = _extract_services_list_helper(services) - messages = [] - if services: - for service in services.keys(): - started = service_resume(service) - if not started: - messages.append("{} didn't start cleanly.".format(service)) - if charm_func: - try: - message = charm_func() - if message: - messages.append(message) - except Exception as e: - message.append(str(e)) + _, messages = manage_payload_services( + 'resume', + services=services, + charm_func=charm_func) clear_unit_paused() if assess_status_func: message = assess_status_func() diff --git a/charmhelpers/contrib/storage/linux/loopback.py b/charmhelpers/contrib/storage/linux/loopback.py index 0dfdae52..82472ff1 100644 --- a/charmhelpers/contrib/storage/linux/loopback.py +++ b/charmhelpers/contrib/storage/linux/loopback.py @@ -36,8 +36,10 @@ def loopback_devices(): ''' loopbacks = {} cmd = ['losetup', '-a'] - devs = [d.strip().split(' ') for d in - check_output(cmd).splitlines() if d != ''] + output = check_output(cmd) + if six.PY3: + output = output.decode('utf-8') + devs = [d.strip().split(' ') for d in output.splitlines() if d != ''] for dev, _, f in devs: loopbacks[dev.replace(':', '')] = re.search(r'\((\S+)\)', f).groups()[0] return loopbacks diff --git a/hooks/horizon_hooks.py b/hooks/horizon_hooks.py index 9221afce..097e8e4c 100755 --- a/hooks/horizon_hooks.py +++ b/hooks/horizon_hooks.py @@ -45,7 +45,6 @@ from charmhelpers.core.hookenv import ( status_set, is_leader, local_unit, - WARNING, network_get, ) from charmhelpers.fetch import ( @@ -68,12 +67,9 @@ from charmhelpers.contrib.openstack.utils import ( series_upgrade_complete, ) from charmhelpers.contrib.openstack.ha.utils import ( - update_dns_ha_resource_params, + generate_ha_relation_data, ) from charmhelpers.contrib.network.ip import ( - get_iface_for_address, - get_netmask_for_address, - is_ipv6, get_relation_ip, ) from charmhelpers.contrib.openstack.cert_utils import ( @@ -81,7 +77,7 @@ from charmhelpers.contrib.openstack.cert_utils import ( process_certificates, ) 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 charmhelpers.contrib.charmsupport import nrpe from charmhelpers.contrib.hardening.harden import harden @@ -189,6 +185,8 @@ def config_changed(): for relid in relation_ids('certificates'): for unit in related_units(relid): certs_changed(relation_id=relid, unit=unit) + for relid in relation_ids('ha'): + ha_relation_joined(relation_id=relid) websso_trusted_dashboard_changed() @@ -228,70 +226,8 @@ def cluster_relation(): @hooks.hook('ha-relation-joined') def ha_relation_joined(relation_id=None): - cluster_config = get_hacluster_config() - resources = { - 'res_horizon_haproxy': 'lsb:haproxy' - } - - resource_params = { - 'res_horizon_haproxy': 'op monitor interval="5s"' - } - - if config('dns-ha'): - update_dns_ha_resource_params(relation_id=relation_id, - resources=resources, - resource_params=resource_params) - else: - vip_group = [] - for vip in cluster_config['vip'].split(): - if is_ipv6(vip): - res_vip = 'ocf:heartbeat:IPv6addr' - vip_params = 'ipv6addr' - else: - res_vip = 'ocf:heartbeat:IPaddr2' - vip_params = 'ip' - - iface = (get_iface_for_address(vip) or - config('vip_iface')) - netmask = (get_netmask_for_address(vip) or - config('vip_cidr')) - - if iface is not None: - vip_key = 'res_horizon_{}_vip'.format(iface) - if vip_key in vip_group: - if vip not in resource_params[vip_key]: - vip_key = '{}_{}'.format(vip_key, vip_params) - else: - log("Resource '%s' (vip='%s') already exists in " - "vip group - skipping" % (vip_key, vip), WARNING) - continue - - resources[vip_key] = res_vip - resource_params[vip_key] = ( - 'params {ip}="{vip}" cidr_netmask="{netmask}"' - ' nic="{iface}"'.format(ip=vip_params, - vip=vip, - iface=iface, - netmask=netmask) - ) - vip_group.append(vip_key) - - if len(vip_group) > 1: - relation_set(groups={'grp_horizon_vips': ' '.join(vip_group)}) - - init_services = { - 'res_horizon_haproxy': 'haproxy' - } - clones = { - 'cl_horizon_haproxy': 'res_horizon_haproxy' - } - relation_set(relation_id=relation_id, - init_services=init_services, - corosync_bindiface=cluster_config['ha-bindiface'], - corosync_mcastport=cluster_config['ha-mcastport'], - resources=resources, - resource_params=resource_params, - clones=clones) + settings = generate_ha_relation_data('horizon') + relation_set(relation_id=relation_id, **settings) @hooks.hook('website-relation-joined') diff --git a/unit_tests/test_horizon_hooks.py b/unit_tests/test_horizon_hooks.py index 13fc34a5..36dcdf66 100644 --- a/unit_tests/test_horizon_hooks.py +++ b/unit_tests/test_horizon_hooks.py @@ -34,8 +34,6 @@ with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec: RESTART_MAP = utils.restart_map() utils.register_configs = _register_configs -from charmhelpers.contrib.hahelpers.cluster import HAIncompleteConfig - TO_PATCH = [ 'config', 'relation_set', @@ -46,7 +44,6 @@ TO_PATCH = [ 'filter_installed_packages', 'open_port', 'CONFIGS', - 'get_hacluster_config', 'relation_ids', 'enable_ssl', 'openstack_upgrade_available', @@ -58,15 +55,13 @@ TO_PATCH = [ 'execd_preinstall', 'b64decode', 'os_release', - 'get_iface_for_address', - 'get_netmask_for_address', 'update_nrpe_config', 'lsb_release', 'status_set', - 'update_dns_ha_resource_params', 'services', 'service_restart', 'remove_old_packages', + 'generate_ha_relation_data', ] @@ -189,107 +184,12 @@ class TestHorizonHooks(CharmTestCase): self.remove_old_packages.assert_called_once_with() self.service_restart.assert_called_once_with('apache2') - def test_ha_joined_complete_config(self): - conf = { - 'ha-bindiface': 'eth100', - 'ha-mcastport': '37373', - 'vip': '192.168.25.163', - 'vip_iface': 'eth101', - 'vip_cidr': '19' - } - self.get_iface_for_address.return_value = 'eth101' - self.get_netmask_for_address.return_value = '19' - self.get_hacluster_config.return_value = conf + def test_ha_joined(self): + self.generate_ha_relation_data.return_value = {'rel_data': 'data'} self._call_hook('ha-relation-joined') - ex_args = { - 'relation_id': None, - 'corosync_mcastport': '37373', - 'init_services': { - 'res_horizon_haproxy': 'haproxy'}, - 'resource_params': { - 'res_horizon_eth101_vip': - 'params ip="192.168.25.163" cidr_netmask="19"' - ' nic="eth101"', - 'res_horizon_haproxy': 'op monitor interval="5s"'}, - 'corosync_bindiface': 'eth100', - 'clones': { - 'cl_horizon_haproxy': 'res_horizon_haproxy'}, - 'resources': { - 'res_horizon_eth101_vip': 'ocf:heartbeat:IPaddr2', - 'res_horizon_haproxy': 'lsb:haproxy'} - } - self.relation_set.assert_called_with(**ex_args) - - def test_ha_joined_no_bound_ip(self): - conf = { - 'ha-bindiface': 'eth100', - 'ha-mcastport': '37373', - 'vip': '192.168.25.163', - } - self.test_config.set('vip_iface', 'eth120') - self.test_config.set('vip_cidr', '21') - self.get_iface_for_address.return_value = None - self.get_netmask_for_address.return_value = None - self.get_hacluster_config.return_value = conf - self._call_hook('ha-relation-joined') - ex_args = { - 'relation_id': None, - 'corosync_mcastport': '37373', - 'init_services': { - 'res_horizon_haproxy': 'haproxy'}, - 'resource_params': { - 'res_horizon_eth120_vip': - 'params ip="192.168.25.163" cidr_netmask="21"' - ' nic="eth120"', - 'res_horizon_haproxy': 'op monitor interval="5s"'}, - 'corosync_bindiface': 'eth100', - 'clones': { - 'cl_horizon_haproxy': 'res_horizon_haproxy'}, - 'resources': { - 'res_horizon_eth120_vip': 'ocf:heartbeat:IPaddr2', - 'res_horizon_haproxy': 'lsb:haproxy'} - } - self.relation_set.assert_called_with(**ex_args) - - def test_ha_joined_incomplete_config(self): - self.get_hacluster_config.side_effect = HAIncompleteConfig(1, 'bang') - self.assertRaises(HAIncompleteConfig, self._call_hook, - 'ha-relation-joined') - - def test_ha_joined_dns_ha(self): - def _fake_update(resources, resource_params, relation_id=None): - resources.update({'res_horizon_public_hostname': 'ocf:maas:dns'}) - resource_params.update({'res_horizon_public_hostname': - 'params fqdn="keystone.maas" ' - 'ip_address="10.0.0.1"'}) - - self.test_config.set('dns-ha', True) - self.get_hacluster_config.return_value = { - 'vip': None, - 'ha-bindiface': 'em0', - 'ha-mcastport': '8080', - 'os-admin-hostname': None, - 'os-internal-hostname': None, - 'os-public-hostname': 'keystone.maas', - } - args = { - 'relation_id': None, - 'corosync_bindiface': 'em0', - 'corosync_mcastport': '8080', - 'init_services': {'res_horizon_haproxy': 'haproxy'}, - 'resources': {'res_horizon_public_hostname': 'ocf:maas:dns', - 'res_horizon_haproxy': 'lsb:haproxy'}, - 'resource_params': { - 'res_horizon_public_hostname': 'params fqdn="keystone.maas" ' - 'ip_address="10.0.0.1"', - 'res_horizon_haproxy': 'op monitor interval="5s"'}, - 'clones': {'cl_horizon_haproxy': 'res_horizon_haproxy'} - } - self.update_dns_ha_resource_params.side_effect = _fake_update - - hooks.ha_relation_joined() - self.assertTrue(self.update_dns_ha_resource_params.called) - self.relation_set.assert_called_with(**args) + self.relation_set.assert_called_once_with( + rel_data='data', + relation_id=None) @patch('hooks.horizon_hooks.check_custom_theme') @patch('hooks.horizon_hooks.keystone_joined') @@ -304,6 +204,7 @@ class TestHorizonHooks(CharmTestCase): 'identity/0', ], 'certificates': [], + 'ha': [], }[rname] self.relation_ids.side_effect = relation_ids_side_effect