From 73128e2d2c961fa5d8b9aea603411027a45adec5 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Wed, 11 May 2016 07:45:50 +0000 Subject: [PATCH 1/7] Add ha class --- lib/charm/openstack/ha.py | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 lib/charm/openstack/ha.py diff --git a/lib/charm/openstack/ha.py b/lib/charm/openstack/ha.py new file mode 100644 index 0000000..ec1ea99 --- /dev/null +++ b/lib/charm/openstack/ha.py @@ -0,0 +1,102 @@ +"""Classes for enabling ha in Openstack charms with the reactive framework""" + +import ipaddress + +import charmhelpers.contrib.network.ip as ip +from charmhelpers.core.hookenv import unit_private_ip, config +from relations.hacluster.common import CRM +from relations.hacluster.common import ResourceDescriptor + +VIP_KEY = "vip" +CIDR_KEY = "vip_cidr" +IFACE_KEY = "vip_iface" + +"""Configure ha resources with: +@when('ha.connected') +def cluster_connected(hacluster): + ha.configure_ha_resources(hacluster, 'designate', ha_resources=['vips', 'haproxy']) + +TODO Proper docs to follow +""" + +def configure_ha_resources(hacluster, service_name, ha_resources=None): + user_config = config() + + RESOURCE_TYPES = { + 'vips': configure_vips, + 'haproxy': configure_haproxy, + } + if not ha_resources: + return + resources = CRM() + for res_type in ha_resources: + resources = RESOURCE_TYPES[res_type](resources, service_name) + hacluster.bind_on(iface=user_config['vip_iface'], mcastport=4440) + hacluster.manage_resources(resources) + +def configure_vips(_resources, service_name): + user_config = config() + + for vip in user_config.get(VIP_KEY, []).split(): + iface = (ip.get_iface_for_address(vip) or + config(IFACE_KEY)) + netmask = (ip.get_netmask_for_address(vip) or + config(CIDR_KEY)) + if iface is not None: + _resources.add( + VirtualIP( + service_name, + vip, + nic=iface, + cidr=netmask + ) + ) + return _resources + +def configure_haproxy(_resources, service_name): + _resources.add( + InitService( + service_name, + 'haproxy', + ) + ) + return _resources + +class InitService(ResourceDescriptor): + def __init__(self, service_name, init_service_name): + self.service_name = service_name + self.init_service_name = init_service_name + + def configure_resource(self, crm): + res_key = 'res_{}_{}'.format( + self.service_name.replace('-', '_'), + self.init_service_name.replace('-', '_') + ) + clone_key = 'cl_{}'.format(res_key) + res_type = 'lsb:{}'.format(self.init_service_name) + crm.primitive(res_key, res_type, params='op monitor interval="5s"') + crm.init_services(self.init_service_name) + crm.clone(clone_key, res_key) + +class VirtualIP(ResourceDescriptor): + def __init__(self, service_name, vip, nic=None, cidr=None): + self.service_name = service_name + self.vip = vip + self.nic = nic + self.cidr = cidr + + def configure_resource(self, crm): + vip_key = 'res_{}_{}_vip'.format(self.service_name, self.nic) + ipaddr = ipaddress.ip_address(self.vip) + if isinstance(ipaddr, ipaddress.IPv4Address): + res_type = 'ocf:heartbeat:IPaddr2' + res_params = 'ip="{}"'.format(self.vip) + else: + res_type = 'ocf:heartbeat:IPv6addr' + res_params = 'ipv6addr="{}"'.format(self.vip) + + if self.nic: + res_params = '{} nic="{}"'.format(res_params, self.nic) + if self.cidr: + res_params = '{} cidr_netmask="{}"'.format(res_params, self.cidr) + crm.primitive(vip_key, res_type, params=res_params) From 2c75b4f728c98cb5f9956b712c20909c365ba30d Mon Sep 17 00:00:00 2001 From: Liam Young Date: Wed, 11 May 2016 14:48:45 +0000 Subject: [PATCH 2/7] Eliminate dangle --- lib/charm/openstack/ha.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/charm/openstack/ha.py b/lib/charm/openstack/ha.py index ec1ea99..fcbf892 100644 --- a/lib/charm/openstack/ha.py +++ b/lib/charm/openstack/ha.py @@ -31,7 +31,8 @@ def configure_ha_resources(hacluster, service_name, ha_resources=None): resources = CRM() for res_type in ha_resources: resources = RESOURCE_TYPES[res_type](resources, service_name) - hacluster.bind_on(iface=user_config['vip_iface'], mcastport=4440) + # TODO Remove hardcoded multicast port + hacluster.bind_on(iface=user_config[IFACE_KEY], mcastport=4440) hacluster.manage_resources(resources) def configure_vips(_resources, service_name): @@ -48,18 +49,14 @@ def configure_vips(_resources, service_name): service_name, vip, nic=iface, - cidr=netmask - ) - ) + cidr=netmask,)) return _resources def configure_haproxy(_resources, service_name): _resources.add( InitService( service_name, - 'haproxy', - ) - ) + 'haproxy',)) return _resources class InitService(ResourceDescriptor): @@ -70,8 +67,7 @@ def __init__(self, service_name, init_service_name): def configure_resource(self, crm): res_key = 'res_{}_{}'.format( self.service_name.replace('-', '_'), - self.init_service_name.replace('-', '_') - ) + self.init_service_name.replace('-', '_')) clone_key = 'cl_{}'.format(res_key) res_type = 'lsb:{}'.format(self.init_service_name) crm.primitive(res_key, res_type, params='op monitor interval="5s"') From 3b5dc9d8c570865855b7291390a8ff4090f00602 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Fri, 13 May 2016 09:49:22 +0000 Subject: [PATCH 3/7] Move method for configuring ha resources into the charm class to simplify calls from charm handler --- lib/charm/openstack/charm.py | 42 ++++++++++++++++++++++++++ lib/charm/openstack/ha.py | 58 +++++------------------------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py index af2ab19..9460beb 100644 --- a/lib/charm/openstack/charm.py +++ b/lib/charm/openstack/charm.py @@ -23,6 +23,13 @@ from charms.reactive.bus import set_state, remove_state from charm.openstack.ip import PUBLIC, INTERNAL, ADMIN, canonical_url +import charmhelpers.contrib.network.ip as ip +import charm.openstack.ha as ha +from relations.hacluster.common import CRM + +VIP_KEY = "vip" +CIDR_KEY = "vip_cidr" +IFACE_KEY = "vip_iface" class OpenStackCharm(object): @@ -51,6 +58,7 @@ class OpenStackCharm(object): restart_map = {} sync_cmd = [] services = [] + ha_resources = [] adapters_class = None def __init__(self, interfaces=None): @@ -150,6 +158,40 @@ def db_sync(self): # render_domain_config needs a working system self.restart_all() + def configure_ha_resources(self, hacluster): + RESOURCE_TYPES = { + 'vips': self.add_ha_vips_config, + 'haproxy': self.add_ha_haproxy_config, + } + self.resources = CRM() + if not self.ha_resources: + return + for res_type in self.ha_resources: + RESOURCE_TYPES[res_type]() + # TODO Remove hardcoded multicast port + hacluster.bind_on(iface=self.config[IFACE_KEY], mcastport=4440) + hacluster.manage_resources(self.resources) + + def add_ha_vips_config(self): + for vip in self.config.get(VIP_KEY, []).split(): + iface = (ip.get_iface_for_address(vip) or + self.config(IFACE_KEY)) + netmask = (ip.get_netmask_for_address(vip) or + self.config(CIDR_KEY)) + if iface is not None: + self.resources.add( + ha.VirtualIP( + self.name, + vip, + nic=iface, + cidr=netmask,)) + + def add_ha_haproxy_config(self): + self.resources.add( + ha.InitService( + self.name, + 'haproxy',)) + class OpenStackCharmFactory(object): diff --git a/lib/charm/openstack/ha.py b/lib/charm/openstack/ha.py index fcbf892..7986c73 100644 --- a/lib/charm/openstack/ha.py +++ b/lib/charm/openstack/ha.py @@ -1,65 +1,20 @@ """Classes for enabling ha in Openstack charms with the reactive framework""" -import ipaddress - -import charmhelpers.contrib.network.ip as ip -from charmhelpers.core.hookenv import unit_private_ip, config -from relations.hacluster.common import CRM -from relations.hacluster.common import ResourceDescriptor +import relations.hacluster.common -VIP_KEY = "vip" -CIDR_KEY = "vip_cidr" -IFACE_KEY = "vip_iface" +import ipaddress """Configure ha resources with: @when('ha.connected') def cluster_connected(hacluster): - ha.configure_ha_resources(hacluster, 'designate', ha_resources=['vips', 'haproxy']) + charm = DesignateCharmFactory.charm() + charm.configure_ha_resources(hacluster) TODO Proper docs to follow """ -def configure_ha_resources(hacluster, service_name, ha_resources=None): - user_config = config() - - RESOURCE_TYPES = { - 'vips': configure_vips, - 'haproxy': configure_haproxy, - } - if not ha_resources: - return - resources = CRM() - for res_type in ha_resources: - resources = RESOURCE_TYPES[res_type](resources, service_name) - # TODO Remove hardcoded multicast port - hacluster.bind_on(iface=user_config[IFACE_KEY], mcastport=4440) - hacluster.manage_resources(resources) -def configure_vips(_resources, service_name): - user_config = config() - - for vip in user_config.get(VIP_KEY, []).split(): - iface = (ip.get_iface_for_address(vip) or - config(IFACE_KEY)) - netmask = (ip.get_netmask_for_address(vip) or - config(CIDR_KEY)) - if iface is not None: - _resources.add( - VirtualIP( - service_name, - vip, - nic=iface, - cidr=netmask,)) - return _resources - -def configure_haproxy(_resources, service_name): - _resources.add( - InitService( - service_name, - 'haproxy',)) - return _resources - -class InitService(ResourceDescriptor): +class InitService(relations.hacluster.common.ResourceDescriptor): def __init__(self, service_name, init_service_name): self.service_name = service_name self.init_service_name = init_service_name @@ -74,7 +29,8 @@ def configure_resource(self, crm): crm.init_services(self.init_service_name) crm.clone(clone_key, res_key) -class VirtualIP(ResourceDescriptor): + +class VirtualIP(relations.hacluster.common.ResourceDescriptor): def __init__(self, service_name, vip, nic=None, cidr=None): self.service_name = service_name self.vip = vip From f68d15485a297ef6cffea399960ae2bba1940277 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Fri, 20 May 2016 08:33:55 +0000 Subject: [PATCH 4/7] Make package lists and restart_map dynamic --- lib/charm/openstack/charm.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py index 9460beb..194ecad 100644 --- a/lib/charm/openstack/charm.py +++ b/lib/charm/openstack/charm.py @@ -40,7 +40,7 @@ class OpenStackCharm(object): name = 'charmname' - packages = [] + base_packages = [] """Packages to install""" api_ports = {} @@ -55,11 +55,12 @@ class OpenStackCharm(object): default_service = None """Default service for the charm""" - restart_map = {} + base_restart_map = {} sync_cmd = [] services = [] ha_resources = [] adapters_class = None + HAPROXY_CONF = '/etc/haproxy/haproxy.cfg' def __init__(self, interfaces=None): self.config = config() @@ -68,6 +69,24 @@ def __init__(self, interfaces=None): if interfaces and self.adapters_class: self.adapter_instance = self.adapters_class(interfaces) + def enable_haproxy(self): + return 'haproxy' in self.ha_resources + + @property + def packages(self): + _packages = [] + _packages.extend(self.base_packages) + if self.enable_haproxy(): + _packages.append('haproxy') + return _packages + + @property + def restart_map(self): + _restart_map = self.base_restart_map.copy() + if self.enable_haproxy(): + _restart_map[self.HAPROXY_CONF] = ['haproxy'] + return _restart_map + def install(self): """ Install packages related to this charm based on From 90890397a5518dc165d8179605b595de8b57c766 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Fri, 20 May 2016 08:44:14 +0000 Subject: [PATCH 5/7] Add config for rendering haproxy --- lib/charm/openstack/adapters.py | 136 +++++++++++++++++++++++++++++++- lib/charm/openstack/charm.py | 9 ++- templates/haproxy.cfg | 38 ++++----- 3 files changed, 160 insertions(+), 23 deletions(-) diff --git a/lib/charm/openstack/adapters.py b/lib/charm/openstack/adapters.py index bcf0f7a..e11a252 100644 --- a/lib/charm/openstack/adapters.py +++ b/lib/charm/openstack/adapters.py @@ -1,6 +1,18 @@ """Adapter classes and utilities for use with Reactive interfaces""" from charmhelpers.core import hookenv +import charms.reactive.bus as reactive_bus +from charmhelpers.contrib.network.ip import ( + get_address_in_network, + get_ipv6_addr, + get_netmask_for_address, +) +from charmhelpers.contrib.openstack.utils import get_host_ip +from charmhelpers.contrib.hahelpers.cluster import ( + determine_apache_port, +) + +ADDRESS_TYPES = ['admin', 'internal', 'public'] class OpenStackRelationAdapter(object): @@ -82,6 +94,44 @@ def hosts(self): return None +class PeerHARelationAdapter(OpenStackRelationAdapter): + """ + """ + + interface_type = "cluster" + + def __init__(self, relation): + super(PeerHARelationAdapter, self).__init__(relation) + self.config = hookenv.config() + self.local_address = APIConfigurationAdapter().local_address + self.local_unit_name = APIConfigurationAdapter().local_unit_name + self.cluster_hosts = {} + self.add_network_split_addresses() + self.add_default_addresses() + + def add_network_split_addresses(self): + for addr_type in ADDRESS_TYPES: + cfg_opt = 'os-{}-network'.format(addr_type) + laddr = get_address_in_network(self.config.get(cfg_opt)) + if laddr: + netmask = get_netmask_for_address(laddr) + self.cluster_hosts[laddr] = { + 'network': "{}/{}".format(laddr, netmask), + 'backends': {self.local_unit_name: laddr}} + key = '{}-address'.format(addr_type) + for _unit, _laddr in self.relation.ip_map(address_key=key): + self.cluster_hosts[laddr]['backends'][_unit] = _laddr + + def add_default_addresses(self): + self.cluster_hosts[self.local_address] = {} + netmask = get_netmask_for_address(self.local_address) + self.cluster_hosts[self.local_address] = { + 'network': "{}/{}".format(self.local_address, netmask), + 'backends': {self.local_unit_name: self.local_address}} + for _unit, _laddr in self.relation.ip_map(): + self.cluster_hosts[self.local_address]['backends'][_unit] = _laddr + + class DatabaseRelationAdapter(OpenStackRelationAdapter): """ Adapter for the Database relation interface. @@ -148,6 +198,87 @@ def __init__(self): setattr(self, k, v) +class APIConfigurationAdapter(ConfigurationAdapter): + + def __init__(self, port_map=None): + super(APIConfigurationAdapter, self).__init__() + self.port_map = port_map + self.config = hookenv.config() + + @property + def ipv6_mode(self): + return self.config.get('prefer-ipv6', False) + + @property + def local_address(self): + if self.ipv6_mode: + addr = get_ipv6_addr(exc_list=[self.config('vip')])[0] + else: + addr = get_host_ip(hookenv.unit_get('private-address')) + return addr + + @property + def local_unit_name(self): + return hookenv.local_unit().replace('/', '-') + + @property + def ipv6_mode(self): + return self.config.get('prefer-ipv6', False) + + @property + def local_host(self): + return 'ip6-localhost' if self.ipv6_mode else '127.0.0.1' + + @property + def haproxy_host(self): + return '::' if self.ipv6_mode else '0.0.0.0' + + @property + def haproxy_stat_port(self): + return '8888' + + @property + def haproxy_stat_password(self): + return reactive_bus.get_state('haproxy.stat.password') + + @property + def service_ports(self): + service_ports = {} + if self.port_map: + for service in self.port_map.keys(): + service_ports[service] = [ + self.port_map[service]['admin'], + determine_apache_port(self.port_map[service]['admin']), + ] + return service_ports + + @property + def service_listen_info(self): + info = {} + if self.port_map: + for service in self.port_map.keys(): + key = service.replace('-', '_') + info[key] = { + 'proto': 'http', + 'ip': self.local_address, + 'port': determine_apache_port(self.port_map[service]['admin'])} + info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key]) + return info + + @property + def external_endpoints(self): + info = {} + ip = self.config.get('vip', self.local_address) + if self.port_map: + for service in self.port_map.keys(): + key = service.replace('-', '_') + info[key] = { + 'proto': 'http', + 'ip': ip, + 'port': self.port_map[service]['admin']} + info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key]) + return info + class OpenStackRelationAdapters(object): """ Base adapters class for OpenStack Charms, used to aggregate @@ -171,13 +302,14 @@ class OpenStackRelationAdapters(object): _adapters = { 'amqp': RabbitMQRelationAdapter, 'shared_db': DatabaseRelationAdapter, + 'cluster': PeerHARelationAdapter, } """ Default adapter mappings; may be overridden by relation adapters in subclasses. """ - def __init__(self, relations, options=ConfigurationAdapter): + def __init__(self, relations, options=ConfigurationAdapter, **kwargs): self._adapters.update(self.relation_adapters) self._relations = [] for relation in relations: @@ -188,7 +320,7 @@ def __init__(self, relations, options=ConfigurationAdapter): relation_value = OpenStackRelationAdapter(relation) setattr(self, relation_name, relation_value) self._relations.append(relation_name) - self.options = options() + self.options = options(**kwargs) self._relations.append('options') def __iter__(self): diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py index 194ecad..55378c2 100644 --- a/lib/charm/openstack/charm.py +++ b/lib/charm/openstack/charm.py @@ -10,7 +10,7 @@ from charmhelpers.contrib.openstack.utils import ( configure_installation_source, ) -from charmhelpers.core.host import path_hash, service_restart +from charmhelpers.core.host import path_hash, service_restart, pwgen from charmhelpers.core.hookenv import config, status_set from charmhelpers.fetch import ( apt_install, @@ -20,7 +20,7 @@ from charmhelpers.contrib.openstack.templating import get_loader from charmhelpers.core.templating import render from charmhelpers.core.hookenv import leader_get, leader_set -from charms.reactive.bus import set_state, remove_state +from charms.reactive.bus import set_state, remove_state, get_state from charm.openstack.ip import PUBLIC, INTERNAL, ADMIN, canonical_url import charmhelpers.contrib.network.ip as ip @@ -68,6 +68,7 @@ def __init__(self, interfaces=None): self.release = 'liberty' if interfaces and self.adapters_class: self.adapter_instance = self.adapters_class(interfaces) + self.set_haproxy_stat_password() def enable_haproxy(self): return 'haproxy' in self.ha_resources @@ -211,6 +212,10 @@ def add_ha_haproxy_config(self): self.name, 'haproxy',)) + def set_haproxy_stat_password(self): + if not get_state('haproxy.stat.password'): + set_state('haproxy.stat.password', pwgen(32)) + class OpenStackCharmFactory(object): diff --git a/templates/haproxy.cfg b/templates/haproxy.cfg index 8721d8a..c2f1caa 100644 --- a/templates/haproxy.cfg +++ b/templates/haproxy.cfg @@ -1,6 +1,6 @@ global - log {{ local_host }} local0 - log {{ local_host }} local1 notice + log {{ options.local_host }} local0 + log {{ options.local_host }} local1 notice maxconn 20000 user haproxy group haproxy @@ -12,52 +12,52 @@ defaults option tcplog option dontlognull retries 3 -{%- if haproxy_queue_timeout %} - timeout queue {{ haproxy_queue_timeout }} +{%- if options.haproxy_queue_timeout %} + timeout queue {{ options.haproxy_queue_timeout }} {%- else %} timeout queue 5000 {%- endif %} -{%- if haproxy_connect_timeout %} - timeout connect {{ haproxy_connect_timeout }} +{%- if options.haproxy_connect_timeout %} + timeout connect {{ options.haproxy_connect_timeout }} {%- else %} timeout connect 5000 {%- endif %} -{%- if haproxy_client_timeout %} - timeout client {{ haproxy_client_timeout }} +{%- if options.haproxy_client_timeout %} + timeout client {{ options.haproxy_client_timeout }} {%- else %} timeout client 30000 {%- endif %} -{%- if haproxy_server_timeout %} - timeout server {{ haproxy_server_timeout }} +{%- if options.haproxy_server_timeout %} + timeout server {{ options.haproxy_server_timeout }} {%- else %} timeout server 30000 {%- endif %} -listen stats {{ stat_port }} +listen stats {{ options.stat_port }} mode http stats enable stats hide-version stats realm Haproxy\ Statistics stats uri / - stats auth admin:password + stats auth admin:{{ options.haproxy_stat_password }} -{% if frontends -%} -{% for service, ports in service_ports.items() -%} +{% if cluster.cluster_hosts -%} +{% for service, ports in options.service_ports.items() -%} frontend tcp-in_{{ service }} bind *:{{ ports[0] }} {% if ipv6 -%} bind :::{{ ports[0] }} {% endif -%} - {% for frontend in frontends -%} - acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }} + {% for frontend in cluster.cluster_hosts -%} + acl net_{{ frontend }} dst {{ cluster.cluster_hosts[frontend]['network'] }} use_backend {{ service }}_{{ frontend }} if net_{{ frontend }} {% endfor -%} - default_backend {{ service }}_{{ default_backend }} + default_backend {{ service }}_{{ cluster.local_address }} -{% for frontend in frontends -%} +{% for frontend in cluster.cluster_hosts -%} backend {{ service }}_{{ frontend }} balance leastconn - {% for unit, address in frontends[frontend]['backends'].items() -%} + {% for unit, address in cluster.cluster_hosts[frontend]['backends'].items() -%} server {{ unit }} {{ address }}:{{ ports[1] }} check {% endfor %} {% endfor -%} From 3891e415875822452f3fdadf1da99cc2f3ff1e86 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 23 May 2016 08:50:36 +0000 Subject: [PATCH 6/7] Add HAPeer interface to all openstackAPI charms and configure haproxy for single nodes deploys --- lib/charm/openstack/adapters.py | 6 ++++-- lib/charm/openstack/charm.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/charm/openstack/adapters.py b/lib/charm/openstack/adapters.py index e11a252..e74a581 100644 --- a/lib/charm/openstack/adapters.py +++ b/lib/charm/openstack/adapters.py @@ -248,7 +248,7 @@ def service_ports(self): for service in self.port_map.keys(): service_ports[service] = [ self.port_map[service]['admin'], - determine_apache_port(self.port_map[service]['admin']), + determine_apache_port(self.port_map[service]['admin'], singlenode_mode=True), ] return service_ports @@ -261,7 +261,9 @@ def service_listen_info(self): info[key] = { 'proto': 'http', 'ip': self.local_address, - 'port': determine_apache_port(self.port_map[service]['admin'])} + 'port': determine_apache_port( + self.port_map[service]['admin'], + singlenode_mode=True)} info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key]) return info diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py index 55378c2..8a9cba0 100644 --- a/lib/charm/openstack/charm.py +++ b/lib/charm/openstack/charm.py @@ -26,6 +26,7 @@ import charmhelpers.contrib.network.ip as ip import charm.openstack.ha as ha from relations.hacluster.common import CRM +import relations.openstack_ha.peers as ha_peers VIP_KEY = "vip" CIDR_KEY = "vip_cidr" @@ -236,6 +237,11 @@ def charm(cls, release=None, interfaces=None): Get an instance of the right charm for the configured OpenStack series """ + cluster_interface = ha_peers.OpenstackHAPeers('cluster') + if interfaces: + interfaces.append(cluster_interface) + else: + interfaces=[cluster_interface] if release and release in cls.releases: return cls.releases[release](interfaces=interfaces) else: From c4db6091437ceb53c0e165e8258dba89b1a54979 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 23 May 2016 10:20:58 +0000 Subject: [PATCH 7/7] Docstrings lint etc --- lib/charm/openstack/adapters.py | 110 +++++++++++++++++++++++++------- lib/charm/openstack/charm.py | 59 +++++++++++++---- lib/charm/openstack/ha.py | 2 +- 3 files changed, 135 insertions(+), 36 deletions(-) diff --git a/lib/charm/openstack/adapters.py b/lib/charm/openstack/adapters.py index e74a581..41d17f8 100644 --- a/lib/charm/openstack/adapters.py +++ b/lib/charm/openstack/adapters.py @@ -1,16 +1,10 @@ """Adapter classes and utilities for use with Reactive interfaces""" -from charmhelpers.core import hookenv import charms.reactive.bus as reactive_bus -from charmhelpers.contrib.network.ip import ( - get_address_in_network, - get_ipv6_addr, - get_netmask_for_address, -) -from charmhelpers.contrib.openstack.utils import get_host_ip -from charmhelpers.contrib.hahelpers.cluster import ( - determine_apache_port, -) +import charmhelpers.contrib.hahelpers.cluster as ch_cluster +import charmhelpers.contrib.network.ip as ch_ip +import charmhelpers.contrib.openstack.utils as ch_utils +import charmhelpers.core.hookenv as hookenv ADDRESS_TYPES = ['admin', 'internal', 'public'] @@ -96,6 +90,7 @@ def hosts(self): class PeerHARelationAdapter(OpenStackRelationAdapter): """ + Adapter for cluster relation of nodes of the same service """ interface_type = "cluster" @@ -110,11 +105,13 @@ def __init__(self, relation): self.add_default_addresses() def add_network_split_addresses(self): + """Populate cluster_hosts with addresses of peers on a given network if + this node is also on that network""" for addr_type in ADDRESS_TYPES: cfg_opt = 'os-{}-network'.format(addr_type) - laddr = get_address_in_network(self.config.get(cfg_opt)) + laddr = ch_ip.get_address_in_network(self.config.get(cfg_opt)) if laddr: - netmask = get_netmask_for_address(laddr) + netmask = ch_ip.get_netmask_for_address(laddr) self.cluster_hosts[laddr] = { 'network': "{}/{}".format(laddr, netmask), 'backends': {self.local_unit_name: laddr}} @@ -123,8 +120,10 @@ def add_network_split_addresses(self): self.cluster_hosts[laddr]['backends'][_unit] = _laddr def add_default_addresses(self): + """Populate cluster_hosts with addresses supplied by private-address + """ self.cluster_hosts[self.local_address] = {} - netmask = get_netmask_for_address(self.local_address) + netmask = ch_ip.get_netmask_for_address(self.local_address) self.cluster_hosts[self.local_address] = { 'network': "{}/{}".format(self.local_address, netmask), 'backends': {self.local_unit_name: self.local_address}} @@ -199,6 +198,8 @@ def __init__(self): class APIConfigurationAdapter(ConfigurationAdapter): + """This configuration adapter extends the base class and adds properties + common accross most OpenstackAPI services""" def __init__(self, port_map=None): super(APIConfigurationAdapter, self).__init__() @@ -207,53 +208,97 @@ def __init__(self, port_map=None): @property def ipv6_mode(self): + """Return if charm should enable IPv6 + + @return True if user has requested ipv6 support otherwise False + """ return self.config.get('prefer-ipv6', False) @property def local_address(self): + """Return remotely accessible address of charm (not localhost) + + @return True if user has requested ipv6 support otherwise False + """ if self.ipv6_mode: - addr = get_ipv6_addr(exc_list=[self.config('vip')])[0] + addr = ch_ip.get_ipv6_addr(exc_list=[self.config('vip')])[0] else: - addr = get_host_ip(hookenv.unit_get('private-address')) + addr = ch_utils.get_host_ip(hookenv.unit_get('private-address')) return addr @property def local_unit_name(self): + """ + @return local unit name + """ return hookenv.local_unit().replace('/', '-') - @property - def ipv6_mode(self): - return self.config.get('prefer-ipv6', False) - @property def local_host(self): + """Return localhost address depending on whether IPv6 is enabled + + @return localhost ip address + """ return 'ip6-localhost' if self.ipv6_mode else '127.0.0.1' @property def haproxy_host(self): + """Return haproxy bind address depending on whether IPv6 is enabled + + @return address + """ return '::' if self.ipv6_mode else '0.0.0.0' @property def haproxy_stat_port(self): + """Port to listen on to access haproxy statistics + + @return port + """ return '8888' @property def haproxy_stat_password(self): + """Password for accessing haproxy statistics + + @return password + """ return reactive_bus.get_state('haproxy.stat.password') @property def service_ports(self): + """Dict of service names and the ports they listen on + + @return {'svc1': 'portA', 'svc2': 'portB', ...} + """ service_ports = {} if self.port_map: for service in self.port_map.keys(): service_ports[service] = [ self.port_map[service]['admin'], - determine_apache_port(self.port_map[service]['admin'], singlenode_mode=True), - ] + ch_cluster.determine_apache_port( + self.port_map[service]['admin'], + singlenode_mode=True)] return service_ports @property def service_listen_info(self): + """Dict of service names and attributes for backend to listen on + + @return { + 'svc1': { + 'proto': 'http', + 'ip': '10.0.0.10', + 'port': '8080', + 'url': 'http://10.0.0.10:8080}, + 'svc2': { + 'proto': 'https', + 'ip': '10.0.0.20', + 'port': '8443', + 'url': 'https://10.0.0.20:8443}, + ... + + """ info = {} if self.port_map: for service in self.port_map.keys(): @@ -261,7 +306,7 @@ def service_listen_info(self): info[key] = { 'proto': 'http', 'ip': self.local_address, - 'port': determine_apache_port( + 'port': ch_cluster.determine_apache_port( self.port_map[service]['admin'], singlenode_mode=True)} info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key]) @@ -269,6 +314,23 @@ def service_listen_info(self): @property def external_endpoints(self): + """Dict of service names and attributes that clients use to connect + + @return { + 'svc1': { + 'proto': 'http', + 'ip': '10.0.0.10', + 'port': '8080', + 'url': 'http://10.0.0.10:8080}, + 'svc2': { + 'proto': 'https', + 'ip': '10.0.0.20', + 'port': '8443', + 'url': 'https://10.0.0.20:8443}, + ... + + """ + info = {} info = {} ip = self.config.get('vip', self.local_address) if self.port_map: @@ -281,6 +343,7 @@ def external_endpoints(self): info[key]['url'] = '{proto}://{ip}:{port}'.format(**info[key]) return info + class OpenStackRelationAdapters(object): """ Base adapters class for OpenStack Charms, used to aggregate @@ -309,6 +372,9 @@ class OpenStackRelationAdapters(object): """ Default adapter mappings; may be overridden by relation adapters in subclasses. + + Additional kwargs can be passed to the configuration adapterwhich has been + specified via the options parameter """ def __init__(self, relations, options=ConfigurationAdapter, **kwargs): diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py index 8a9cba0..43ab284 100644 --- a/lib/charm/openstack/charm.py +++ b/lib/charm/openstack/charm.py @@ -11,7 +11,7 @@ configure_installation_source, ) from charmhelpers.core.host import path_hash, service_restart, pwgen -from charmhelpers.core.hookenv import config, status_set +from charmhelpers.core.hookenv import config, status_set, relation_ids from charmhelpers.fetch import ( apt_install, apt_update, @@ -42,7 +42,7 @@ class OpenStackCharm(object): name = 'charmname' base_packages = [] - """Packages to install""" + """Packages to install unconditionally""" api_ports = {} """ @@ -57,6 +57,9 @@ class OpenStackCharm(object): """Default service for the charm""" base_restart_map = {} + """Map of services which must always be restarted when corresponding + configuration file changes + """ sync_cmd = [] services = [] ha_resources = [] @@ -72,10 +75,17 @@ def __init__(self, interfaces=None): self.set_haproxy_stat_password() def enable_haproxy(self): + """Determine if haproxy is fronting the services + + @return True if haproxy is fronting the service""" return 'haproxy' in self.ha_resources @property def packages(self): + """List of packages to be installed + + @return ['pkg1', 'pkg2', ...] + """ _packages = [] _packages.extend(self.base_packages) if self.enable_haproxy(): @@ -84,6 +94,14 @@ def packages(self): @property def restart_map(self): + """Map of services to be restarted if a file changes + + @return { + 'file1': ['svc1', 'svc3'], + 'file2': ['svc2', 'svc3'], + ... + } + """ _restart_map = self.base_restart_map.copy() if self.enable_haproxy(): _restart_map[self.HAPROXY_CONF] = ['haproxy'] @@ -180,9 +198,14 @@ def db_sync(self): self.restart_all() def configure_ha_resources(self, hacluster): + """Inform the ha subordinate about each service it should manage. The + child class specifies the services via self.ha_resources + + @param hacluster interface + """ RESOURCE_TYPES = { - 'vips': self.add_ha_vips_config, - 'haproxy': self.add_ha_haproxy_config, + 'vips': self._add_ha_vips_config, + 'haproxy': self._add_ha_haproxy_config, } self.resources = CRM() if not self.ha_resources: @@ -193,7 +216,9 @@ def configure_ha_resources(self, hacluster): hacluster.bind_on(iface=self.config[IFACE_KEY], mcastport=4440) hacluster.manage_resources(self.resources) - def add_ha_vips_config(self): + def _add_ha_vips_config(self): + """Add a VirtualIP object for each user specified vip to self.resources + """ for vip in self.config.get(VIP_KEY, []).split(): iface = (ip.get_iface_for_address(vip) or self.config(IFACE_KEY)) @@ -207,13 +232,16 @@ def add_ha_vips_config(self): nic=iface, cidr=netmask,)) - def add_ha_haproxy_config(self): + def _add_ha_haproxy_config(self): + """Add a InitService object for haproxy to self.resources + """ self.resources.add( ha.InitService( self.name, 'haproxy',)) def set_haproxy_stat_password(self): + """Set a stats password for accessing haproxy statistics""" if not get_state('haproxy.stat.password'): set_state('haproxy.stat.password', pwgen(32)) @@ -234,14 +262,19 @@ class OpenStackCharmFactory(object): @classmethod def charm(cls, release=None, interfaces=None): """ - Get an instance of the right charm for the - configured OpenStack series + Get an instance of the right charm for the configured OpenStack series + + If the cluster relation exists add the cluster interface. It is + forecfully added here as the interface is needed even if there is only + one unit in the service. If only one unit exists the cluster hooks + never fire. """ - cluster_interface = ha_peers.OpenstackHAPeers('cluster') - if interfaces: - interfaces.append(cluster_interface) - else: - interfaces=[cluster_interface] + if relation_ids('cluster'): + cluster_interface = ha_peers.OpenstackHAPeers('cluster') + if interfaces: + interfaces.append(cluster_interface) + else: + interfaces = [cluster_interface] if release and release in cls.releases: return cls.releases[release](interfaces=interfaces) else: diff --git a/lib/charm/openstack/ha.py b/lib/charm/openstack/ha.py index 7986c73..2aee2db 100644 --- a/lib/charm/openstack/ha.py +++ b/lib/charm/openstack/ha.py @@ -1,8 +1,8 @@ """Classes for enabling ha in Openstack charms with the reactive framework""" +import ipaddress import relations.hacluster.common -import ipaddress """Configure ha resources with: @when('ha.connected')