Sync charm-helpers

Notable issues resolved:

openstack_upgrade_available() broken for swift
https://bugs.launchpad.net/charm-swift-proxy/+bug/1743847

haproxy context doesn't consider bindings
https://bugs.launchpad.net/charm-helpers/+bug/1735421

regression in haproxy check
https://bugs.launchpad.net/charm-helpers/+bug/1743287

This change also breaks the inheritance between
HorizonHAProxyContext and HAProxyContext (as this was
not needed) and aligns the code in HorizonHAProxyContext
to the behaviour in HAProxyContext of using get_relation_ip
instead of unit_get.

Change-Id: I72600b01744a07e140a29255efc2823ea39ce730
This commit is contained in:
Ryan Beisner 2018-01-19 12:10:37 +00:00 committed by James Page
parent b4019e45bf
commit f36b718162
15 changed files with 324 additions and 88 deletions

View File

@ -27,6 +27,7 @@ from charmhelpers.core.hookenv import (
network_get_primary_address, network_get_primary_address,
unit_get, unit_get,
WARNING, WARNING,
NoNetworkBinding,
) )
from charmhelpers.core.host import ( from charmhelpers.core.host import (
@ -578,6 +579,9 @@ def get_relation_ip(interface, cidr_network=None):
except NotImplementedError: except NotImplementedError:
# If network-get is not available # If network-get is not available
address = get_host_ip(unit_get('private-address')) address = get_host_ip(unit_get('private-address'))
except NoNetworkBinding:
log("No network binding for {}".format(interface), WARNING)
address = get_host_ip(unit_get('private-address'))
if config('prefer-ipv6'): if config('prefer-ipv6'):
# Currently IPv6 has priority, eventually we want IPv6 to just be # Currently IPv6 has priority, eventually we want IPv6 to just be

View File

@ -93,10 +93,10 @@ from charmhelpers.contrib.network.ip import (
format_ipv6_addr, format_ipv6_addr,
is_bridge_member, is_bridge_member,
is_ipv6_disabled, is_ipv6_disabled,
get_relation_ip,
) )
from charmhelpers.contrib.openstack.utils import ( from charmhelpers.contrib.openstack.utils import (
config_flags_parser, config_flags_parser,
get_host_ip,
enable_memcache, enable_memcache,
snap_install_requested, snap_install_requested,
CompareOpenStackReleases, CompareOpenStackReleases,
@ -617,7 +617,9 @@ class HAProxyContext(OSContextGenerator):
""" """
interfaces = ['cluster'] interfaces = ['cluster']
def __init__(self, singlenode_mode=False): def __init__(self, singlenode_mode=False,
address_types=ADDRESS_TYPES):
self.address_types = address_types
self.singlenode_mode = singlenode_mode self.singlenode_mode = singlenode_mode
def __call__(self): def __call__(self):
@ -626,19 +628,22 @@ class HAProxyContext(OSContextGenerator):
if not relation_ids('cluster') and not self.singlenode_mode: if not relation_ids('cluster') and not self.singlenode_mode:
return {} return {}
if config('prefer-ipv6'):
addr = get_ipv6_addr(exc_list=[config('vip')])[0]
else:
addr = get_host_ip(unit_get('private-address'))
l_unit = local_unit().replace('/', '-') l_unit = local_unit().replace('/', '-')
cluster_hosts = {} cluster_hosts = {}
# NOTE(jamespage): build out map of configured network endpoints # NOTE(jamespage): build out map of configured network endpoints
# and associated backends # and associated backends
for addr_type in ADDRESS_TYPES: for addr_type in self.address_types:
cfg_opt = 'os-{}-network'.format(addr_type) cfg_opt = 'os-{}-network'.format(addr_type)
laddr = get_address_in_network(config(cfg_opt)) # NOTE(thedac) For some reason the ADDRESS_MAP uses 'int' rather
# than 'internal'
if addr_type == 'internal':
_addr_map_type = INTERNAL
else:
_addr_map_type = addr_type
# Network spaces aware
laddr = get_relation_ip(ADDRESS_MAP[_addr_map_type]['binding'],
config(cfg_opt))
if laddr: if laddr:
netmask = get_netmask_for_address(laddr) netmask = get_netmask_for_address(laddr)
cluster_hosts[laddr] = { cluster_hosts[laddr] = {
@ -649,15 +654,19 @@ class HAProxyContext(OSContextGenerator):
} }
for rid in relation_ids('cluster'): for rid in relation_ids('cluster'):
for unit in sorted(related_units(rid)): for unit in sorted(related_units(rid)):
# API Charms will need to set {addr_type}-address with
# get_relation_ip(addr_type)
_laddr = relation_get('{}-address'.format(addr_type), _laddr = relation_get('{}-address'.format(addr_type),
rid=rid, unit=unit) rid=rid, unit=unit)
if _laddr: if _laddr:
_unit = unit.replace('/', '-') _unit = unit.replace('/', '-')
cluster_hosts[laddr]['backends'][_unit] = _laddr cluster_hosts[laddr]['backends'][_unit] = _laddr
# NOTE(jamespage) add backend based on private address - this # NOTE(jamespage) add backend based on get_relation_ip - this
# with either be the only backend or the fallback if no acls # will either be the only backend or the fallback if no acls
# match in the frontend # match in the frontend
# Network spaces aware
addr = get_relation_ip('cluster')
cluster_hosts[addr] = {} cluster_hosts[addr] = {}
netmask = get_netmask_for_address(addr) netmask = get_netmask_for_address(addr)
cluster_hosts[addr] = { cluster_hosts[addr] = {
@ -667,6 +676,8 @@ class HAProxyContext(OSContextGenerator):
} }
for rid in relation_ids('cluster'): for rid in relation_ids('cluster'):
for unit in sorted(related_units(rid)): for unit in sorted(related_units(rid)):
# API Charms will need to set their private-address with
# get_relation_ip('cluster')
_laddr = relation_get('private-address', _laddr = relation_get('private-address',
rid=rid, unit=unit) rid=rid, unit=unit)
if _laddr: if _laddr:
@ -1775,3 +1786,30 @@ class MemcacheContext(OSContextGenerator):
ctxt['memcache_server_formatted'], ctxt['memcache_server_formatted'],
ctxt['memcache_port']) ctxt['memcache_port'])
return ctxt return ctxt
class EnsureDirContext(OSContextGenerator):
'''
Serves as a generic context to create a directory as a side-effect.
Useful for software that supports drop-in files (.d) in conjunction
with config option-based templates. Examples include:
* OpenStack oslo.policy drop-in files;
* systemd drop-in config files;
* other software that supports overriding defaults with .d files
Another use-case is when a subordinate generates a configuration for
primary to render in a separate directory.
Some software requires a user to create a target directory to be
scanned for drop-in files with a specific format. This is why this
context is needed to do that before rendering a template.
'''
def __init__(self, dirname):
'''Used merely to ensure that a given directory exists.'''
self.dirname = dirname
def __call__(self):
mkdir(self.dirname)
return {}

View File

@ -9,7 +9,7 @@
CRITICAL=0 CRITICAL=0
NOTACTIVE='' NOTACTIVE=''
LOGFILE=/var/log/nagios/check_haproxy.log LOGFILE=/var/log/nagios/check_haproxy.log
AUTH=$(grep -r "stats auth" /etc/haproxy/haproxy.cfg | awk 'NR=1{print $4}') AUTH=$(grep -r "stats auth" /etc/haproxy/haproxy.cfg | awk 'NR=1{print $3}')
typeset -i N_INSTANCES=0 typeset -i N_INSTANCES=0
for appserver in $(awk '/^\s+server/{print $2}' /etc/haproxy/haproxy.cfg) for appserver in $(awk '/^\s+server/{print $2}' /etc/haproxy/haproxy.cfg)

View File

@ -10,7 +10,7 @@
CURRQthrsh=0 CURRQthrsh=0
MAXQthrsh=100 MAXQthrsh=100
AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}') AUTH=$(grep -r "stats auth" /etc/haproxy/haproxy.cfg | awk 'NR=1{print $3}')
HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v) HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)

View File

@ -23,6 +23,8 @@
Helpers for high availability. Helpers for high availability.
""" """
import json
import re import re
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
@ -32,6 +34,7 @@ from charmhelpers.core.hookenv import (
config, config,
status_set, status_set,
DEBUG, DEBUG,
WARNING,
) )
from charmhelpers.core.host import ( from charmhelpers.core.host import (
@ -40,6 +43,23 @@ from charmhelpers.core.host import (
from charmhelpers.contrib.openstack.ip import ( from charmhelpers.contrib.openstack.ip import (
resolve_address, resolve_address,
is_ipv6,
)
from charmhelpers.contrib.network.ip import (
get_iface_for_address,
get_netmask_for_address,
)
from charmhelpers.contrib.hahelpers.cluster import (
get_hacluster_config
)
JSON_ENCODE_OPTIONS = dict(
sort_keys=True,
allow_nan=False,
indent=None,
separators=(',', ':'),
) )
@ -53,8 +73,8 @@ class DNSHAException(Exception):
def update_dns_ha_resource_params(resources, resource_params, def update_dns_ha_resource_params(resources, resource_params,
relation_id=None, relation_id=None,
crm_ocf='ocf:maas:dns'): crm_ocf='ocf:maas:dns'):
""" Check for os-*-hostname settings and update resource dictionaries for """ Configure DNS-HA resources based on provided configuration and
the HA relation. update resource dictionaries for the HA relation.
@param resources: Pointer to dictionary of resources. @param resources: Pointer to dictionary of resources.
Usually instantiated in ha_joined(). Usually instantiated in ha_joined().
@ -64,7 +84,85 @@ def update_dns_ha_resource_params(resources, resource_params,
@param crm_ocf: Corosync Open Cluster Framework resource agent to use for @param crm_ocf: Corosync Open Cluster Framework resource agent to use for
DNS HA DNS HA
""" """
_relation_data = {'resources': {}, 'resource_params': {}}
update_hacluster_dns_ha(charm_name(),
_relation_data,
crm_ocf)
resources.update(_relation_data['resources'])
resource_params.update(_relation_data['resource_params'])
relation_set(relation_id=relation_id, groups=_relation_data['groups'])
def assert_charm_supports_dns_ha():
"""Validate prerequisites for DNS HA
The MAAS client is only available on Xenial or greater
:raises DNSHAException: if release is < 16.04
"""
if lsb_release().get('DISTRIB_RELEASE') < '16.04':
msg = ('DNS HA is only supported on 16.04 and greater '
'versions of Ubuntu.')
status_set('blocked', msg)
raise DNSHAException(msg)
return True
def expect_ha():
""" Determine if the unit expects to be in HA
Check for VIP or dns-ha settings which indicate the unit should expect to
be related to hacluster.
@returns boolean
"""
return config('vip') or config('dns-ha')
def generate_ha_relation_data(service):
""" 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.
@returns dict: json encoded data for use with relation_set
"""
_haproxy_res = 'res_{}_haproxy'.format(service)
_relation_data = {
'resources': {
_haproxy_res: 'lsb:haproxy',
},
'resource_params': {
_haproxy_res: 'op monitor interval="5s"'
},
'init_services': {
_haproxy_res: 'haproxy'
},
'clones': {
'cl_{}_haproxy'.format(service): _haproxy_res
},
}
if config('dns-ha'):
update_hacluster_dns_ha(service, _relation_data)
else:
update_hacluster_vip(service, _relation_data)
return {
'json_{}'.format(k): json.dumps(v, **JSON_ENCODE_OPTIONS)
for k, v in _relation_data.items() if v
}
def update_hacluster_dns_ha(service, relation_data,
crm_ocf='ocf:maas:dns'):
""" Configure DNS-HA resources based on provided configuration
@param service: Name of the service being configured
@param relation_data: Pointer to dictionary of relation data.
@param crm_ocf: Corosync Open Cluster Framework resource agent to use for
DNS HA
"""
# Validate the charm environment for DNS HA # Validate the charm environment for DNS HA
assert_charm_supports_dns_ha() assert_charm_supports_dns_ha()
@ -93,7 +191,7 @@ def update_dns_ha_resource_params(resources, resource_params,
status_set('blocked', msg) status_set('blocked', msg)
raise DNSHAException(msg) raise DNSHAException(msg)
hostname_key = 'res_{}_{}_hostname'.format(charm_name(), endpoint_type) hostname_key = 'res_{}_{}_hostname'.format(service, endpoint_type)
if hostname_key in hostname_group: if hostname_key in hostname_group:
log('DNS HA: Resource {}: {} already exists in ' log('DNS HA: Resource {}: {} already exists in '
'hostname group - skipping'.format(hostname_key, hostname), 'hostname group - skipping'.format(hostname_key, hostname),
@ -101,42 +199,67 @@ def update_dns_ha_resource_params(resources, resource_params,
continue continue
hostname_group.append(hostname_key) hostname_group.append(hostname_key)
resources[hostname_key] = crm_ocf relation_data['resources'][hostname_key] = crm_ocf
resource_params[hostname_key] = ( relation_data['resource_params'][hostname_key] = (
'params fqdn="{}" ip_address="{}" ' 'params fqdn="{}" ip_address="{}"'
''.format(hostname, resolve_address(endpoint_type=endpoint_type, .format(hostname, resolve_address(endpoint_type=endpoint_type,
override=False))) override=False)))
if len(hostname_group) >= 1: if len(hostname_group) >= 1:
log('DNS HA: Hostname group is set with {} as members. ' log('DNS HA: Hostname group is set with {} as members. '
'Informing the ha relation'.format(' '.join(hostname_group)), 'Informing the ha relation'.format(' '.join(hostname_group)),
DEBUG) DEBUG)
relation_set(relation_id=relation_id, groups={ relation_data['groups'] = {
'grp_{}_hostnames'.format(charm_name()): ' '.join(hostname_group)}) 'grp_{}_hostnames'.format(service): ' '.join(hostname_group)
}
else: else:
msg = 'DNS HA: Hostname group has no members.' msg = 'DNS HA: Hostname group has no members.'
status_set('blocked', msg) status_set('blocked', msg)
raise DNSHAException(msg) raise DNSHAException(msg)
def assert_charm_supports_dns_ha(): def update_hacluster_vip(service, relation_data):
"""Validate prerequisites for DNS HA """ Configure VIP resources based on provided configuration
The MAAS client is only available on Xenial or greater
@param service: Name of the service being configured
@param relation_data: Pointer to dictionary of relation data.
""" """
if lsb_release().get('DISTRIB_RELEASE') < '16.04': cluster_config = get_hacluster_config()
msg = ('DNS HA is only supported on 16.04 and greater ' vip_group = []
'versions of Ubuntu.') for vip in cluster_config['vip'].split():
status_set('blocked', msg) if is_ipv6(vip):
raise DNSHAException(msg) res_neutron_vip = 'ocf:heartbeat:IPv6addr'
return True vip_params = 'ipv6addr'
else:
res_neutron_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'))
def expect_ha(): if iface is not None:
""" Determine if the unit expects to be in HA 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
Check for VIP or dns-ha settings which indicate the unit should expect to relation_data['resources'][vip_key] = res_neutron_vip
be related to hacluster. 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)
@returns boolean if len(vip_group) >= 1:
""" relation_data['groups'] = {
return config('vip') or config('dns-ha') 'grp_{}_vips'.format(service): ' '.join(vip_group)
}

View File

@ -15,9 +15,6 @@ Listen {{ public_port }}
{% if port -%} {% if port -%}
<VirtualHost *:{{ port }}> <VirtualHost *:{{ port }}>
WSGIDaemonProcess {{ service_name }} processes={{ processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ WSGIDaemonProcess {{ service_name }} processes={{ processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
{% if python_path -%}
python-path={{ python_path }} \
{% endif -%}
display-name=%{GROUP} display-name=%{GROUP}
WSGIProcessGroup {{ service_name }} WSGIProcessGroup {{ service_name }}
WSGIScriptAlias / {{ script }} WSGIScriptAlias / {{ script }}
@ -29,7 +26,7 @@ Listen {{ public_port }}
ErrorLog /var/log/apache2/{{ service_name }}_error.log ErrorLog /var/log/apache2/{{ service_name }}_error.log
CustomLog /var/log/apache2/{{ service_name }}_access.log combined CustomLog /var/log/apache2/{{ service_name }}_access.log combined
<Directory {{ usr_bin }}> <Directory /usr/bin>
<IfVersion >= 2.4> <IfVersion >= 2.4>
Require all granted Require all granted
</IfVersion> </IfVersion>
@ -44,9 +41,6 @@ Listen {{ public_port }}
{% if admin_port -%} {% if admin_port -%}
<VirtualHost *:{{ admin_port }}> <VirtualHost *:{{ admin_port }}>
WSGIDaemonProcess {{ service_name }}-admin processes={{ admin_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ WSGIDaemonProcess {{ service_name }}-admin processes={{ admin_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
{% if python_path -%}
python-path={{ python_path }} \
{% endif -%}
display-name=%{GROUP} display-name=%{GROUP}
WSGIProcessGroup {{ service_name }}-admin WSGIProcessGroup {{ service_name }}-admin
WSGIScriptAlias / {{ admin_script }} WSGIScriptAlias / {{ admin_script }}
@ -58,7 +52,7 @@ Listen {{ public_port }}
ErrorLog /var/log/apache2/{{ service_name }}_error.log ErrorLog /var/log/apache2/{{ service_name }}_error.log
CustomLog /var/log/apache2/{{ service_name }}_access.log combined CustomLog /var/log/apache2/{{ service_name }}_access.log combined
<Directory {{ usr_bin }}> <Directory /usr/bin>
<IfVersion >= 2.4> <IfVersion >= 2.4>
Require all granted Require all granted
</IfVersion> </IfVersion>
@ -73,9 +67,6 @@ Listen {{ public_port }}
{% if public_port -%} {% if public_port -%}
<VirtualHost *:{{ public_port }}> <VirtualHost *:{{ public_port }}>
WSGIDaemonProcess {{ service_name }}-public processes={{ public_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ WSGIDaemonProcess {{ service_name }}-public processes={{ public_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
{% if python_path -%}
python-path={{ python_path }} \
{% endif -%}
display-name=%{GROUP} display-name=%{GROUP}
WSGIProcessGroup {{ service_name }}-public WSGIProcessGroup {{ service_name }}-public
WSGIScriptAlias / {{ public_script }} WSGIScriptAlias / {{ public_script }}
@ -87,7 +78,7 @@ Listen {{ public_port }}
ErrorLog /var/log/apache2/{{ service_name }}_error.log ErrorLog /var/log/apache2/{{ service_name }}_error.log
CustomLog /var/log/apache2/{{ service_name }}_access.log combined CustomLog /var/log/apache2/{{ service_name }}_access.log combined
<Directory {{ usr_bin }}> <Directory /usr/bin>
<IfVersion >= 2.4> <IfVersion >= 2.4>
Require all granted Require all granted
</IfVersion> </IfVersion>

View File

@ -93,7 +93,8 @@ class OSConfigTemplate(object):
Associates a config file template with a list of context generators. Associates a config file template with a list of context generators.
Responsible for constructing a template context based on those generators. Responsible for constructing a template context based on those generators.
""" """
def __init__(self, config_file, contexts):
def __init__(self, config_file, contexts, config_template=None):
self.config_file = config_file self.config_file = config_file
if hasattr(contexts, '__call__'): if hasattr(contexts, '__call__'):
@ -103,6 +104,8 @@ class OSConfigTemplate(object):
self._complete_contexts = [] self._complete_contexts = []
self.config_template = config_template
def context(self): def context(self):
ctxt = {} ctxt = {}
for context in self.contexts: for context in self.contexts:
@ -124,6 +127,11 @@ class OSConfigTemplate(object):
self.context() self.context()
return self._complete_contexts return self._complete_contexts
@property
def is_string_template(self):
""":returns: Boolean if this instance is a template initialised with a string"""
return self.config_template is not None
class OSConfigRenderer(object): class OSConfigRenderer(object):
""" """
@ -148,6 +156,10 @@ class OSConfigRenderer(object):
contexts=[context.IdentityServiceContext()]) contexts=[context.IdentityServiceContext()])
configs.register(config_file='/etc/haproxy/haproxy.conf', configs.register(config_file='/etc/haproxy/haproxy.conf',
contexts=[context.HAProxyContext()]) contexts=[context.HAProxyContext()])
configs.register(config_file='/etc/keystone/policy.d/extra.cfg',
contexts=[context.ExtraPolicyContext()
context.KeystoneContext()],
config_template=hookenv.config('extra-policy'))
# write out a single config # write out a single config
configs.write('/etc/nova/nova.conf') configs.write('/etc/nova/nova.conf')
# write out all registered configs # write out all registered configs
@ -218,14 +230,23 @@ class OSConfigRenderer(object):
else: else:
apt_install('python3-jinja2') apt_install('python3-jinja2')
def register(self, config_file, contexts): def register(self, config_file, contexts, config_template=None):
""" """
Register a config file with a list of context generators to be called Register a config file with a list of context generators to be called
during rendering. during rendering.
config_template can be used to load a template from a string instead of
using template loaders and template files.
:param config_file (str): a path where a config file will be rendered
:param contexts (list): a list of context dictionaries with kv pairs
:param config_template (str): an optional template string to use
""" """
self.templates[config_file] = OSConfigTemplate(config_file=config_file, self.templates[config_file] = OSConfigTemplate(
contexts=contexts) config_file=config_file,
log('Registered config file: %s' % config_file, level=INFO) contexts=contexts,
config_template=config_template
)
log('Registered config file: {}'.format(config_file),
level=INFO)
def _get_tmpl_env(self): def _get_tmpl_env(self):
if not self._tmpl_env: if not self._tmpl_env:
@ -235,32 +256,58 @@ class OSConfigRenderer(object):
def _get_template(self, template): def _get_template(self, template):
self._get_tmpl_env() self._get_tmpl_env()
template = self._tmpl_env.get_template(template) template = self._tmpl_env.get_template(template)
log('Loaded template from %s' % template.filename, level=INFO) log('Loaded template from {}'.format(template.filename),
level=INFO)
return template
def _get_template_from_string(self, ostmpl):
'''
Get a jinja2 template object from a string.
:param ostmpl: OSConfigTemplate to use as a data source.
'''
self._get_tmpl_env()
template = self._tmpl_env.from_string(ostmpl.config_template)
log('Loaded a template from a string for {}'.format(
ostmpl.config_file),
level=INFO)
return template return template
def render(self, config_file): def render(self, config_file):
if config_file not in self.templates: if config_file not in self.templates:
log('Config not registered: %s' % config_file, level=ERROR) log('Config not registered: {}'.format(config_file), level=ERROR)
raise OSConfigException raise OSConfigException
ctxt = self.templates[config_file].context()
_tmpl = os.path.basename(config_file) ostmpl = self.templates[config_file]
try: ctxt = ostmpl.context()
template = self._get_template(_tmpl)
except exceptions.TemplateNotFound: if ostmpl.is_string_template:
# if no template is found with basename, try looking for it template = self._get_template_from_string(ostmpl)
# using a munged full path, eg: log('Rendering from a string template: '
# /etc/apache2/apache2.conf -> etc_apache2_apache2.conf '{}'.format(config_file),
_tmpl = '_'.join(config_file.split('/')[1:]) level=INFO)
else:
_tmpl = os.path.basename(config_file)
try: try:
template = self._get_template(_tmpl) template = self._get_template(_tmpl)
except exceptions.TemplateNotFound as e: except exceptions.TemplateNotFound:
log('Could not load template from %s by %s or %s.' % # if no template is found with basename, try looking
(self.templates_dir, os.path.basename(config_file), _tmpl), # for it using a munged full path, eg:
level=ERROR) # /etc/apache2/apache2.conf -> etc_apache2_apache2.conf
raise e _tmpl = '_'.join(config_file.split('/')[1:])
try:
template = self._get_template(_tmpl)
except exceptions.TemplateNotFound as e:
log('Could not load template from {} by {} or {}.'
''.format(
self.templates_dir,
os.path.basename(config_file),
_tmpl
),
level=ERROR)
raise e
log('Rendering from template: %s' % _tmpl, level=INFO) log('Rendering from template: {}'.format(config_file),
level=INFO)
return template.render(ctxt) return template.render(ctxt)
def write(self, config_file): def write(self, config_file):

View File

@ -626,11 +626,6 @@ def openstack_upgrade_available(package):
else: else:
avail_vers = get_os_version_install_source(src) avail_vers = get_os_version_install_source(src)
apt.init() apt.init()
if "swift" in package:
major_cur_vers = cur_vers.split('.', 1)[0]
major_avail_vers = avail_vers.split('.', 1)[0]
major_diff = apt.version_compare(major_avail_vers, major_cur_vers)
return avail_vers > cur_vers and (major_diff == 1 or major_diff == 0)
return apt.version_compare(avail_vers, cur_vers) == 1 return apt.version_compare(avail_vers, cur_vers) == 1

View File

@ -39,6 +39,7 @@ if not six.PY3:
else: else:
from collections import UserDict from collections import UserDict
CRITICAL = "CRITICAL" CRITICAL = "CRITICAL"
ERROR = "ERROR" ERROR = "ERROR"
WARNING = "WARNING" WARNING = "WARNING"
@ -344,6 +345,7 @@ class Config(dict):
""" """
with open(self.path, 'w') as f: with open(self.path, 'w') as f:
os.fchmod(f.fileno(), 0o600)
json.dump(self, f) json.dump(self, f)
def _implicit_save(self): def _implicit_save(self):
@ -818,6 +820,10 @@ class Hooks(object):
return wrapper return wrapper
class NoNetworkBinding(Exception):
pass
def charm_dir(): def charm_dir():
"""Return the root directory of the current charm""" """Return the root directory of the current charm"""
d = os.environ.get('JUJU_CHARM_DIR') d = os.environ.get('JUJU_CHARM_DIR')
@ -1104,7 +1110,17 @@ def network_get_primary_address(binding):
:raise: NotImplementedError if run on Juju < 2.0 :raise: NotImplementedError if run on Juju < 2.0
''' '''
cmd = ['network-get', '--primary-address', binding] cmd = ['network-get', '--primary-address', binding]
return subprocess.check_output(cmd).decode('UTF-8').strip() try:
response = subprocess.check_output(
cmd,
stderr=subprocess.STDOUT).decode('UTF-8').strip()
except CalledProcessError as e:
if 'no network config found for binding' in e.output.decode('UTF-8'):
raise NoNetworkBinding("No network binding for {}"
.format(binding))
else:
raise
return response
@translate_exc(from_exc=OSError, to_exc=NotImplementedError) @translate_exc(from_exc=OSError, to_exc=NotImplementedError)

View File

@ -175,6 +175,8 @@ class Storage(object):
else: else:
self.db_path = os.path.join( self.db_path = os.path.join(
os.environ.get('CHARM_DIR', ''), '.unit-state.db') os.environ.get('CHARM_DIR', ''), '.unit-state.db')
with open(self.db_path, 'a') as f:
os.fchmod(f.fileno(), 0o600)
self.conn = sqlite3.connect('%s' % self.db_path) self.conn = sqlite3.connect('%s' % self.db_path)
self.cursor = self.conn.cursor() self.cursor = self.conn.cursor()
self.revision = None self.revision = None

View File

@ -20,7 +20,6 @@ from charmhelpers.core.hookenv import (
related_units, related_units,
relation_get, relation_get,
local_unit, local_unit,
unit_get,
log, log,
WARNING, WARNING,
ERROR, ERROR,
@ -28,7 +27,6 @@ from charmhelpers.core.hookenv import (
from charmhelpers.core.strutils import bool_from_string from charmhelpers.core.strutils import bool_from_string
from charmhelpers.contrib.openstack.context import ( from charmhelpers.contrib.openstack.context import (
OSContextGenerator, OSContextGenerator,
HAProxyContext,
context_complete context_complete
) )
from charmhelpers.contrib.hahelpers.apache import ( from charmhelpers.contrib.hahelpers.apache import (
@ -39,6 +37,7 @@ from charmhelpers.contrib.hahelpers.apache import (
from charmhelpers.contrib.network.ip import ( from charmhelpers.contrib.network.ip import (
get_ipv6_addr, get_ipv6_addr,
format_ipv6_addr, format_ipv6_addr,
get_relation_ip,
) )
from charmhelpers.core.host import pwgen from charmhelpers.core.host import pwgen
@ -53,7 +52,7 @@ VALID_ENDPOINT_TYPES = {
} }
class HorizonHAProxyContext(HAProxyContext): class HorizonHAProxyContext(OSContextGenerator):
def __call__(self): def __call__(self):
''' '''
Horizon specific HAProxy context; haproxy is used all the time Horizon specific HAProxy context; haproxy is used all the time
@ -65,7 +64,7 @@ class HorizonHAProxyContext(HAProxyContext):
if config('prefer-ipv6'): if config('prefer-ipv6'):
cluster_hosts[l_unit] = get_ipv6_addr(exc_list=[config('vip')])[0] cluster_hosts[l_unit] = get_ipv6_addr(exc_list=[config('vip')])[0]
else: else:
cluster_hosts[l_unit] = unit_get('private-address') cluster_hosts[l_unit] = get_relation_ip('cluster')
for rid in relation_ids('cluster'): for rid in relation_ids('cluster'):
for unit in related_units(rid): for unit in related_units(rid):

View File

@ -131,7 +131,8 @@ CONFIG_FILES = OrderedDict([
(HAPROXY_CONF, { (HAPROXY_CONF, {
'hook_contexts': [ 'hook_contexts': [
horizon_contexts.HorizonHAProxyContext(), horizon_contexts.HorizonHAProxyContext(),
context.HAProxyContext(singlenode_mode=True), context.HAProxyContext(singlenode_mode=True,
address_types=[]),
], ],
'services': ['haproxy'], 'services': ['haproxy'],
}), }),

View File

@ -39,6 +39,7 @@ if not six.PY3:
else: else:
from collections import UserDict from collections import UserDict
CRITICAL = "CRITICAL" CRITICAL = "CRITICAL"
ERROR = "ERROR" ERROR = "ERROR"
WARNING = "WARNING" WARNING = "WARNING"
@ -344,6 +345,7 @@ class Config(dict):
""" """
with open(self.path, 'w') as f: with open(self.path, 'w') as f:
os.fchmod(f.fileno(), 0o600)
json.dump(self, f) json.dump(self, f)
def _implicit_save(self): def _implicit_save(self):
@ -818,6 +820,10 @@ class Hooks(object):
return wrapper return wrapper
class NoNetworkBinding(Exception):
pass
def charm_dir(): def charm_dir():
"""Return the root directory of the current charm""" """Return the root directory of the current charm"""
d = os.environ.get('JUJU_CHARM_DIR') d = os.environ.get('JUJU_CHARM_DIR')
@ -1104,7 +1110,17 @@ def network_get_primary_address(binding):
:raise: NotImplementedError if run on Juju < 2.0 :raise: NotImplementedError if run on Juju < 2.0
''' '''
cmd = ['network-get', '--primary-address', binding] cmd = ['network-get', '--primary-address', binding]
return subprocess.check_output(cmd).decode('UTF-8').strip() try:
response = subprocess.check_output(
cmd,
stderr=subprocess.STDOUT).decode('UTF-8').strip()
except CalledProcessError as e:
if 'no network config found for binding' in e.output.decode('UTF-8'):
raise NoNetworkBinding("No network binding for {}"
.format(binding))
else:
raise
return response
@translate_exc(from_exc=OSError, to_exc=NotImplementedError) @translate_exc(from_exc=OSError, to_exc=NotImplementedError)

View File

@ -175,6 +175,8 @@ class Storage(object):
else: else:
self.db_path = os.path.join( self.db_path = os.path.join(
os.environ.get('CHARM_DIR', ''), '.unit-state.db') os.environ.get('CHARM_DIR', ''), '.unit-state.db')
with open(self.db_path, 'a') as f:
os.fchmod(f.fileno(), 0o600)
self.conn = sqlite3.connect('%s' % self.db_path) self.conn = sqlite3.connect('%s' % self.db_path)
self.cursor = self.conn.cursor() self.cursor = self.conn.cursor()
self.revision = None self.revision = None

View File

@ -30,7 +30,7 @@ TO_PATCH = [
'b64decode', 'b64decode',
'context_complete', 'context_complete',
'local_unit', 'local_unit',
'unit_get', 'get_relation_ip',
'pwgen', 'pwgen',
] ]
@ -597,7 +597,7 @@ class TestHorizonContexts(CharmTestCase):
def test_HorizonHAProxyContext_no_cluster(self): def test_HorizonHAProxyContext_no_cluster(self):
self.relation_ids.return_value = [] self.relation_ids.return_value = []
self.local_unit.return_value = 'openstack-dashboard/0' self.local_unit.return_value = 'openstack-dashboard/0'
self.unit_get.return_value = "10.5.0.1" self.get_relation_ip.return_value = "10.5.0.1"
with patch_open() as (_open, _file): with patch_open() as (_open, _file):
self.assertEqual(horizon_contexts.HorizonHAProxyContext()(), self.assertEqual(horizon_contexts.HorizonHAProxyContext()(),
{'units': {'openstack-dashboard-0': '10.5.0.1'}, {'units': {'openstack-dashboard-0': '10.5.0.1'},
@ -606,6 +606,7 @@ class TestHorizonContexts(CharmTestCase):
'prefer_ipv6': False}) 'prefer_ipv6': False})
_open.assert_called_with('/etc/default/haproxy', 'w') _open.assert_called_with('/etc/default/haproxy', 'w')
self.assertTrue(_file.write.called) self.assertTrue(_file.write.called)
self.get_relation_ip.assert_called_with('cluster')
def test_HorizonHAProxyContext_clustered(self): def test_HorizonHAProxyContext_clustered(self):
self.relation_ids.return_value = ['cluster:0'] self.relation_ids.return_value = ['cluster:0']
@ -614,7 +615,7 @@ class TestHorizonContexts(CharmTestCase):
] ]
self.relation_get.side_effect = ['10.5.0.2', '10.5.0.3'] self.relation_get.side_effect = ['10.5.0.2', '10.5.0.3']
self.local_unit.return_value = 'openstack-dashboard/0' self.local_unit.return_value = 'openstack-dashboard/0'
self.unit_get.return_value = "10.5.0.1" self.get_relation_ip.return_value = "10.5.0.1"
with patch_open() as (_open, _file): with patch_open() as (_open, _file):
self.assertEqual(horizon_contexts.HorizonHAProxyContext()(), self.assertEqual(horizon_contexts.HorizonHAProxyContext()(),
{'units': {'openstack-dashboard-0': '10.5.0.1', {'units': {'openstack-dashboard-0': '10.5.0.1',
@ -625,6 +626,7 @@ class TestHorizonContexts(CharmTestCase):
'prefer_ipv6': False}) 'prefer_ipv6': False})
_open.assert_called_with('/etc/default/haproxy', 'w') _open.assert_called_with('/etc/default/haproxy', 'w')
self.assertTrue(_file.write.called) self.assertTrue(_file.write.called)
self.get_relation_ip.assert_called_with('cluster')
def test_RouterSettingContext(self): def test_RouterSettingContext(self):
self.test_config.set('profile', 'cisco') self.test_config.set('profile', 'cisco')