Sync with charm-helpers.
This commit is contained in:
parent
b195b505cc
commit
71ce7250f9
@ -20,20 +20,27 @@ from charmhelpers.core.hookenv import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_cert():
|
def get_cert(cn=None):
|
||||||
|
# TODO: deal with multiple https endpoints via charm config
|
||||||
cert = config_get('ssl_cert')
|
cert = config_get('ssl_cert')
|
||||||
key = config_get('ssl_key')
|
key = config_get('ssl_key')
|
||||||
if not (cert and key):
|
if not (cert and key):
|
||||||
log("Inspecting identity-service relations for SSL certificate.",
|
log("Inspecting identity-service relations for SSL certificate.",
|
||||||
level=INFO)
|
level=INFO)
|
||||||
cert = key = None
|
cert = key = None
|
||||||
|
if cn:
|
||||||
|
ssl_cert_attr = 'ssl_cert_{}'.format(cn)
|
||||||
|
ssl_key_attr = 'ssl_key_{}'.format(cn)
|
||||||
|
else:
|
||||||
|
ssl_cert_attr = 'ssl_cert'
|
||||||
|
ssl_key_attr = 'ssl_key'
|
||||||
for r_id in relation_ids('identity-service'):
|
for r_id in relation_ids('identity-service'):
|
||||||
for unit in relation_list(r_id):
|
for unit in relation_list(r_id):
|
||||||
if not cert:
|
if not cert:
|
||||||
cert = relation_get('ssl_cert',
|
cert = relation_get(ssl_cert_attr,
|
||||||
rid=r_id, unit=unit)
|
rid=r_id, unit=unit)
|
||||||
if not key:
|
if not key:
|
||||||
key = relation_get('ssl_key',
|
key = relation_get(ssl_key_attr,
|
||||||
rid=r_id, unit=unit)
|
rid=r_id, unit=unit)
|
||||||
return (cert, key)
|
return (cert, key)
|
||||||
|
|
||||||
|
@ -139,10 +139,9 @@ def https():
|
|||||||
return True
|
return True
|
||||||
for r_id in relation_ids('identity-service'):
|
for r_id in relation_ids('identity-service'):
|
||||||
for unit in relation_list(r_id):
|
for unit in relation_list(r_id):
|
||||||
|
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
|
||||||
rel_state = [
|
rel_state = [
|
||||||
relation_get('https_keystone', rid=r_id, unit=unit),
|
relation_get('https_keystone', rid=r_id, unit=unit),
|
||||||
relation_get('ssl_cert', rid=r_id, unit=unit),
|
|
||||||
relation_get('ssl_key', rid=r_id, unit=unit),
|
|
||||||
relation_get('ca_cert', rid=r_id, unit=unit),
|
relation_get('ca_cert', rid=r_id, unit=unit),
|
||||||
]
|
]
|
||||||
# NOTE: works around (LP: #1203241)
|
# NOTE: works around (LP: #1203241)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import glob
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from charmhelpers.fetch import apt_install
|
from charmhelpers.fetch import apt_install
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
ERROR, log, config,
|
ERROR, log,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -156,19 +157,102 @@ get_iface_for_address = partial(_get_for_address, key='iface')
|
|||||||
get_netmask_for_address = partial(_get_for_address, key='netmask')
|
get_netmask_for_address = partial(_get_for_address, key='netmask')
|
||||||
|
|
||||||
|
|
||||||
def get_ipv6_addr(iface="eth0"):
|
def format_ipv6_addr(address):
|
||||||
|
"""
|
||||||
|
IPv6 needs to be wrapped with [] in url link to parse correctly.
|
||||||
|
"""
|
||||||
|
if is_ipv6(address):
|
||||||
|
address = "[%s]" % address
|
||||||
|
else:
|
||||||
|
log("Not an valid ipv6 address: %s" % address,
|
||||||
|
level=ERROR)
|
||||||
|
address = None
|
||||||
|
return address
|
||||||
|
|
||||||
|
|
||||||
|
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=True, exc_list=None):
|
||||||
|
"""
|
||||||
|
Return the assigned IP address for a given interface, if any, or [].
|
||||||
|
"""
|
||||||
|
# Extract nic if passed /dev/ethX
|
||||||
|
if '/' in iface:
|
||||||
|
iface = iface.split('/')[-1]
|
||||||
|
if not exc_list:
|
||||||
|
exc_list = []
|
||||||
try:
|
try:
|
||||||
iface_addrs = netifaces.ifaddresses(iface)
|
inet_num = getattr(netifaces, inet_type)
|
||||||
if netifaces.AF_INET6 not in iface_addrs:
|
except AttributeError:
|
||||||
raise Exception("Interface '%s' doesn't have an ipv6 address." % iface)
|
raise Exception('Unknown inet type ' + str(inet_type))
|
||||||
|
|
||||||
addresses = netifaces.ifaddresses(iface)[netifaces.AF_INET6]
|
interfaces = netifaces.interfaces()
|
||||||
ipv6_addr = [a['addr'] for a in addresses if not a['addr'].startswith('fe80')
|
if inc_aliases:
|
||||||
and config('vip') != a['addr']]
|
ifaces = []
|
||||||
if not ipv6_addr:
|
for _iface in interfaces:
|
||||||
|
if iface == _iface or _iface.split(':')[0] == iface:
|
||||||
|
ifaces.append(_iface)
|
||||||
|
if fatal and not ifaces:
|
||||||
|
raise Exception("Invalid interface '%s'" % iface)
|
||||||
|
ifaces.sort()
|
||||||
|
else:
|
||||||
|
if iface not in interfaces:
|
||||||
|
if fatal:
|
||||||
|
raise Exception("%s not found " % (iface))
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
ifaces = [iface]
|
||||||
|
|
||||||
|
addresses = []
|
||||||
|
for netiface in ifaces:
|
||||||
|
net_info = netifaces.ifaddresses(netiface)
|
||||||
|
if inet_num in net_info:
|
||||||
|
for entry in net_info[inet_num]:
|
||||||
|
if 'addr' in entry and entry['addr'] not in exc_list:
|
||||||
|
addresses.append(entry['addr'])
|
||||||
|
if fatal and not addresses:
|
||||||
|
raise Exception("Interface '%s' doesn't have any %s addresses." % (iface, inet_type))
|
||||||
|
return addresses
|
||||||
|
|
||||||
|
get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
|
||||||
|
|
||||||
|
|
||||||
|
def get_ipv6_addr(iface='eth0', inc_aliases=False, fatal=True, exc_list=None):
|
||||||
|
"""
|
||||||
|
Return the assigned IPv6 address for a given interface, if any, or [].
|
||||||
|
"""
|
||||||
|
addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
|
||||||
|
inc_aliases=inc_aliases, fatal=fatal,
|
||||||
|
exc_list=exc_list)
|
||||||
|
remotly_addressable = []
|
||||||
|
for address in addresses:
|
||||||
|
if not address.startswith('fe80'):
|
||||||
|
remotly_addressable.append(address)
|
||||||
|
if fatal and not remotly_addressable:
|
||||||
raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
|
raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
|
||||||
|
return remotly_addressable
|
||||||
|
|
||||||
return ipv6_addr[0]
|
|
||||||
|
|
||||||
except ValueError:
|
def get_bridges(vnic_dir='/sys/devices/virtual/net'):
|
||||||
raise ValueError("Invalid interface '%s'" % iface)
|
"""
|
||||||
|
Return a list of bridges on the system or []
|
||||||
|
"""
|
||||||
|
b_rgex = vnic_dir + '/*/bridge'
|
||||||
|
return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_rgex)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
|
||||||
|
"""
|
||||||
|
Return a list of nics comprising a given bridge on the system or []
|
||||||
|
"""
|
||||||
|
brif_rgex = "%s/%s/brif/*" % (vnic_dir, bridge)
|
||||||
|
return [x.split('/')[-1] for x in glob.glob(brif_rgex)]
|
||||||
|
|
||||||
|
|
||||||
|
def is_bridge_member(nic):
|
||||||
|
"""
|
||||||
|
Check if a given nic is a member of a bridge
|
||||||
|
"""
|
||||||
|
for bridge in get_bridges():
|
||||||
|
if nic in get_bridge_nics(bridge):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -10,32 +10,60 @@ class OpenStackAmuletDeployment(AmuletDeployment):
|
|||||||
that is specifically for use by OpenStack charms.
|
that is specifically for use by OpenStack charms.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, series=None, openstack=None, source=None):
|
def __init__(self, series=None, openstack=None, source=None, stable=True):
|
||||||
"""Initialize the deployment environment."""
|
"""Initialize the deployment environment."""
|
||||||
super(OpenStackAmuletDeployment, self).__init__(series)
|
super(OpenStackAmuletDeployment, self).__init__(series)
|
||||||
self.openstack = openstack
|
self.openstack = openstack
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.stable = stable
|
||||||
|
# Note(coreycb): this needs to be changed when new next branches come out.
|
||||||
|
self.current_next = "trusty"
|
||||||
|
|
||||||
|
def _determine_branch_locations(self, other_services):
|
||||||
|
"""Determine the branch locations for the other services.
|
||||||
|
|
||||||
|
Determine if the local branch being tested is derived from its
|
||||||
|
stable or next (dev) branch, and based on this, use the corresonding
|
||||||
|
stable or next branches for the other_services."""
|
||||||
|
base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
|
||||||
|
|
||||||
|
if self.stable:
|
||||||
|
for svc in other_services:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
for svc in other_services:
|
||||||
|
if svc['name'] in base_charms:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
temp = 'lp:~openstack-charmers/charms/{}/{}/next'
|
||||||
|
svc['location'] = temp.format(self.current_next,
|
||||||
|
svc['name'])
|
||||||
|
return other_services
|
||||||
|
|
||||||
def _add_services(self, this_service, other_services):
|
def _add_services(self, this_service, other_services):
|
||||||
"""Add services to the deployment and set openstack-origin."""
|
"""Add services to the deployment and set openstack-origin/source."""
|
||||||
|
other_services = self._determine_branch_locations(other_services)
|
||||||
|
|
||||||
super(OpenStackAmuletDeployment, self)._add_services(this_service,
|
super(OpenStackAmuletDeployment, self)._add_services(this_service,
|
||||||
other_services)
|
other_services)
|
||||||
name = 0
|
|
||||||
services = other_services
|
services = other_services
|
||||||
services.append(this_service)
|
services.append(this_service)
|
||||||
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
|
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
|
||||||
|
|
||||||
if self.openstack:
|
if self.openstack:
|
||||||
for svc in services:
|
for svc in services:
|
||||||
if svc[name] not in use_source:
|
if svc['name'] not in use_source:
|
||||||
config = {'openstack-origin': self.openstack}
|
config = {'openstack-origin': self.openstack}
|
||||||
self.d.configure(svc[name], config)
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
if self.source:
|
if self.source:
|
||||||
for svc in services:
|
for svc in services:
|
||||||
if svc[name] in use_source:
|
if svc['name'] in use_source:
|
||||||
config = {'source': self.source}
|
config = {'source': self.source}
|
||||||
self.d.configure(svc[name], config)
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
def _configure_services(self, configs):
|
def _configure_services(self, configs):
|
||||||
"""Configure all of the services."""
|
"""Configure all of the services."""
|
||||||
|
@ -187,15 +187,16 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
|
|
||||||
f = opener.open("http://download.cirros-cloud.net/version/released")
|
f = opener.open("http://download.cirros-cloud.net/version/released")
|
||||||
version = f.read().strip()
|
version = f.read().strip()
|
||||||
cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
|
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
|
||||||
|
local_path = os.path.join('tests', cirros_img)
|
||||||
|
|
||||||
if not os.path.exists(cirros_img):
|
if not os.path.exists(local_path):
|
||||||
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
|
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
|
||||||
version, cirros_img)
|
version, cirros_img)
|
||||||
opener.retrieve(cirros_url, cirros_img)
|
opener.retrieve(cirros_url, local_path)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
with open(cirros_img) as f:
|
with open(local_path) as f:
|
||||||
image = glance.images.create(name=image_name, is_public=True,
|
image = glance.images.create(name=image_name, is_public=True,
|
||||||
disk_format='qcow2',
|
disk_format='qcow2',
|
||||||
container_format='bare', data=f)
|
container_format='bare', data=f)
|
||||||
|
@ -8,7 +8,6 @@ from subprocess import (
|
|||||||
check_call
|
check_call
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
apt_install,
|
apt_install,
|
||||||
filter_installed_packages,
|
filter_installed_packages,
|
||||||
@ -28,6 +27,11 @@ from charmhelpers.core.hookenv import (
|
|||||||
INFO
|
INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.host import (
|
||||||
|
mkdir,
|
||||||
|
write_file
|
||||||
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.hahelpers.cluster import (
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
determine_apache_port,
|
determine_apache_port,
|
||||||
determine_api_port,
|
determine_api_port,
|
||||||
@ -38,6 +42,7 @@ from charmhelpers.contrib.hahelpers.cluster import (
|
|||||||
from charmhelpers.contrib.hahelpers.apache import (
|
from charmhelpers.contrib.hahelpers.apache import (
|
||||||
get_cert,
|
get_cert,
|
||||||
get_ca_cert,
|
get_ca_cert,
|
||||||
|
install_ca_cert,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.openstack.neutron import (
|
from charmhelpers.contrib.openstack.neutron import (
|
||||||
@ -47,6 +52,7 @@ from charmhelpers.contrib.openstack.neutron import (
|
|||||||
from charmhelpers.contrib.network.ip import (
|
from charmhelpers.contrib.network.ip import (
|
||||||
get_address_in_network,
|
get_address_in_network,
|
||||||
get_ipv6_addr,
|
get_ipv6_addr,
|
||||||
|
is_address_in_network
|
||||||
)
|
)
|
||||||
|
|
||||||
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
||||||
@ -421,6 +427,11 @@ class HAProxyContext(OSContextGenerator):
|
|||||||
'units': cluster_hosts,
|
'units': cluster_hosts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config('haproxy-server-timeout'):
|
||||||
|
ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
|
||||||
|
if config('haproxy-client-timeout'):
|
||||||
|
ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
|
||||||
|
|
||||||
if config('prefer-ipv6'):
|
if config('prefer-ipv6'):
|
||||||
ctxt['local_host'] = 'ip6-localhost'
|
ctxt['local_host'] = 'ip6-localhost'
|
||||||
ctxt['haproxy_host'] = '::'
|
ctxt['haproxy_host'] = '::'
|
||||||
@ -490,22 +501,36 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
|
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
|
||||||
check_call(cmd)
|
check_call(cmd)
|
||||||
|
|
||||||
def configure_cert(self):
|
def configure_cert(self, cn=None):
|
||||||
if not os.path.isdir('/etc/apache2/ssl'):
|
|
||||||
os.mkdir('/etc/apache2/ssl')
|
|
||||||
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
|
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
|
||||||
if not os.path.isdir(ssl_dir):
|
mkdir(path=ssl_dir)
|
||||||
os.mkdir(ssl_dir)
|
cert, key = get_cert(cn)
|
||||||
cert, key = get_cert()
|
if cn:
|
||||||
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
|
cert_filename = 'cert_{}'.format(cn)
|
||||||
cert_out.write(b64decode(cert))
|
key_filename = 'key_{}'.format(cn)
|
||||||
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
|
else:
|
||||||
key_out.write(b64decode(key))
|
cert_filename = 'cert'
|
||||||
|
key_filename = 'key'
|
||||||
|
write_file(path=os.path.join(ssl_dir, cert_filename),
|
||||||
|
content=b64decode(cert))
|
||||||
|
write_file(path=os.path.join(ssl_dir, key_filename),
|
||||||
|
content=b64decode(key))
|
||||||
|
|
||||||
|
def configure_ca(self):
|
||||||
ca_cert = get_ca_cert()
|
ca_cert = get_ca_cert()
|
||||||
if ca_cert:
|
if ca_cert:
|
||||||
with open(CA_CERT_PATH, 'w') as ca_out:
|
install_ca_cert(b64decode(ca_cert))
|
||||||
ca_out.write(b64decode(ca_cert))
|
|
||||||
check_call(['update-ca-certificates'])
|
def canonical_names(self):
|
||||||
|
'''Figure out which canonical names clients will access this service'''
|
||||||
|
cns = []
|
||||||
|
for r_id in relation_ids('identity-service'):
|
||||||
|
for unit in related_units(r_id):
|
||||||
|
rdata = relation_get(rid=r_id, unit=unit)
|
||||||
|
for k in rdata:
|
||||||
|
if k.startswith('ssl_key_'):
|
||||||
|
cns.append(k.lstrip('ssl_key_'))
|
||||||
|
return list(set(cns))
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
if isinstance(self.external_ports, basestring):
|
if isinstance(self.external_ports, basestring):
|
||||||
@ -513,21 +538,47 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
if (not self.external_ports or not https()):
|
if (not self.external_ports or not https()):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
self.configure_cert()
|
self.configure_ca()
|
||||||
self.enable_modules()
|
self.enable_modules()
|
||||||
|
|
||||||
ctxt = {
|
ctxt = {
|
||||||
'namespace': self.service_namespace,
|
'namespace': self.service_namespace,
|
||||||
'private_address': unit_get('private-address'),
|
'endpoints': [],
|
||||||
'endpoints': []
|
'ext_ports': []
|
||||||
}
|
}
|
||||||
if is_clustered():
|
|
||||||
ctxt['private_address'] = config('vip')
|
for cn in self.canonical_names():
|
||||||
|
self.configure_cert(cn)
|
||||||
|
|
||||||
|
addresses = []
|
||||||
|
vips = []
|
||||||
|
if config('vip'):
|
||||||
|
vips = config('vip').split()
|
||||||
|
|
||||||
|
for network_type in ['os-internal-network',
|
||||||
|
'os-admin-network',
|
||||||
|
'os-public-network']:
|
||||||
|
address = get_address_in_network(config(network_type),
|
||||||
|
unit_get('private-address'))
|
||||||
|
if len(vips) > 0 and is_clustered():
|
||||||
|
for vip in vips:
|
||||||
|
if is_address_in_network(config(network_type),
|
||||||
|
vip):
|
||||||
|
addresses.append((address, vip))
|
||||||
|
break
|
||||||
|
elif is_clustered():
|
||||||
|
addresses.append((address, config('vip')))
|
||||||
|
else:
|
||||||
|
addresses.append((address, address))
|
||||||
|
|
||||||
|
for address, endpoint in set(addresses):
|
||||||
for api_port in self.external_ports:
|
for api_port in self.external_ports:
|
||||||
ext_port = determine_apache_port(api_port)
|
ext_port = determine_apache_port(api_port)
|
||||||
int_port = determine_api_port(api_port)
|
int_port = determine_api_port(api_port)
|
||||||
portmap = (int(ext_port), int(int_port))
|
portmap = (address, endpoint, int(ext_port), int(int_port))
|
||||||
ctxt['endpoints'].append(portmap)
|
ctxt['endpoints'].append(portmap)
|
||||||
|
ctxt['ext_ports'].append(int(ext_port))
|
||||||
|
ctxt['ext_ports'] = list(set(ctxt['ext_ports']))
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,8 +14,17 @@ defaults
|
|||||||
retries 3
|
retries 3
|
||||||
timeout queue 1000
|
timeout queue 1000
|
||||||
timeout connect 1000
|
timeout connect 1000
|
||||||
|
{% if haproxy_client_timeout -%}
|
||||||
|
timeout client {{ haproxy_client_timeout }}
|
||||||
|
{% else -%}
|
||||||
timeout client 30000
|
timeout client 30000
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% if haproxy_server_timeout -%}
|
||||||
|
timeout server {{ haproxy_server_timeout }}
|
||||||
|
{% else -%}
|
||||||
timeout server 30000
|
timeout server 30000
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
listen stats {{ stat_port }}
|
listen stats {{ stat_port }}
|
||||||
mode http
|
mode http
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
{% if endpoints -%}
|
{% if endpoints -%}
|
||||||
{% for ext, int in endpoints -%}
|
{% for ext_port in ext_ports -%}
|
||||||
Listen {{ ext }}
|
Listen {{ ext_port }}
|
||||||
NameVirtualHost *:{{ ext }}
|
{% endfor -%}
|
||||||
<VirtualHost *:{{ ext }}>
|
{% for address, endpoint, ext, int in endpoints -%}
|
||||||
ServerName {{ private_address }}
|
<VirtualHost {{ address }}:{{ ext }}>
|
||||||
|
ServerName {{ endpoint }}
|
||||||
SSLEngine on
|
SSLEngine on
|
||||||
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
|
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
|
||||||
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
|
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
|
||||||
ProxyPass / http://localhost:{{ int }}/
|
ProxyPass / http://localhost:{{ int }}/
|
||||||
ProxyPassReverse / http://localhost:{{ int }}/
|
ProxyPassReverse / http://localhost:{{ int }}/
|
||||||
ProxyPreserveHost on
|
ProxyPreserveHost on
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
{% endfor -%}
|
||||||
<Proxy *>
|
<Proxy *>
|
||||||
Order deny,allow
|
Order deny,allow
|
||||||
Allow from all
|
Allow from all
|
||||||
@ -19,5 +21,4 @@ NameVirtualHost *:{{ ext }}
|
|||||||
Order allow,deny
|
Order allow,deny
|
||||||
Allow from all
|
Allow from all
|
||||||
</Location>
|
</Location>
|
||||||
{% endfor -%}
|
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
{% if endpoints -%}
|
{% if endpoints -%}
|
||||||
{% for ext, int in endpoints -%}
|
{% for ext_port in ext_ports -%}
|
||||||
Listen {{ ext }}
|
Listen {{ ext_port }}
|
||||||
NameVirtualHost *:{{ ext }}
|
{% endfor -%}
|
||||||
<VirtualHost *:{{ ext }}>
|
{% for address, endpoint, ext, int in endpoints -%}
|
||||||
ServerName {{ private_address }}
|
<VirtualHost {{ address }}:{{ ext }}>
|
||||||
|
ServerName {{ endpoint }}
|
||||||
SSLEngine on
|
SSLEngine on
|
||||||
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
|
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
|
||||||
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
|
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
|
||||||
ProxyPass / http://localhost:{{ int }}/
|
ProxyPass / http://localhost:{{ int }}/
|
||||||
ProxyPassReverse / http://localhost:{{ int }}/
|
ProxyPassReverse / http://localhost:{{ int }}/
|
||||||
ProxyPreserveHost on
|
ProxyPreserveHost on
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
{% endfor -%}
|
||||||
<Proxy *>
|
<Proxy *>
|
||||||
Order deny,allow
|
Order deny,allow
|
||||||
Allow from all
|
Allow from all
|
||||||
@ -19,5 +21,4 @@ NameVirtualHost *:{{ ext }}
|
|||||||
Order allow,deny
|
Order allow,deny
|
||||||
Allow from all
|
Allow from all
|
||||||
</Location>
|
</Location>
|
||||||
{% endfor -%}
|
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
@ -70,6 +70,7 @@ SWIFT_CODENAMES = OrderedDict([
|
|||||||
('1.13.0', 'icehouse'),
|
('1.13.0', 'icehouse'),
|
||||||
('1.12.0', 'icehouse'),
|
('1.12.0', 'icehouse'),
|
||||||
('1.11.0', 'icehouse'),
|
('1.11.0', 'icehouse'),
|
||||||
|
('2.0.0', 'juno'),
|
||||||
])
|
])
|
||||||
|
|
||||||
DEFAULT_LOOPBACK_SIZE = '5G'
|
DEFAULT_LOOPBACK_SIZE = '5G'
|
||||||
|
@ -7,40 +7,38 @@ from charmhelpers.core.hookenv import (
|
|||||||
relation_set,
|
relation_set,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This helper provides functions to support use of a peer relation
|
This helper provides functions to support use of a peer relation
|
||||||
for basic key/value storage, with the added benefit that all storage
|
for basic key/value storage, with the added benefit that all storage
|
||||||
can be replicated across peer units, so this is really useful for
|
can be replicated across peer units.
|
||||||
services that issue usernames/passwords to remote services.
|
|
||||||
|
|
||||||
def shared_db_changed()
|
Requirement to use:
|
||||||
# Only the lead unit should create passwords
|
|
||||||
if not is_leader():
|
|
||||||
return
|
|
||||||
username = relation_get('username')
|
|
||||||
key = '{}.password'.format(username)
|
|
||||||
# Attempt to retrieve any existing password for this user
|
|
||||||
password = peer_retrieve(key)
|
|
||||||
if password is None:
|
|
||||||
# New user, create password and store
|
|
||||||
password = pwgen(length=64)
|
|
||||||
peer_store(key, password)
|
|
||||||
create_access(username, password)
|
|
||||||
relation_set(password=password)
|
|
||||||
|
|
||||||
|
To use this, the "peer_echo()" method has to be called form the peer
|
||||||
|
relation's relation-changed hook:
|
||||||
|
|
||||||
def cluster_changed()
|
@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
|
||||||
# Echo any relation data other that *-address
|
def cluster_relation_changed():
|
||||||
# back onto the peer relation so all units have
|
|
||||||
# all *.password keys stored on their local relation
|
|
||||||
# for later retrieval.
|
|
||||||
peer_echo()
|
peer_echo()
|
||||||
|
|
||||||
|
Once this is done, you can use peer storage from anywhere:
|
||||||
|
|
||||||
|
@hooks.hook("some-hook")
|
||||||
|
def some_hook():
|
||||||
|
# You can store and retrieve key/values this way:
|
||||||
|
if is_relation_made("cluster"): # from charmhelpers.core.hookenv
|
||||||
|
# There are peers available so we can work with peer storage
|
||||||
|
peer_store("mykey", "myvalue")
|
||||||
|
value = peer_retrieve("mykey")
|
||||||
|
print value
|
||||||
|
else:
|
||||||
|
print "No peers joind the relation, cannot share key/values :("
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def peer_retrieve(key, relation_name='cluster'):
|
def peer_retrieve(key, relation_name='cluster'):
|
||||||
""" Retrieve a named key from peer relation relation_name """
|
"""Retrieve a named key from peer relation `relation_name`."""
|
||||||
cluster_rels = relation_ids(relation_name)
|
cluster_rels = relation_ids(relation_name)
|
||||||
if len(cluster_rels) > 0:
|
if len(cluster_rels) > 0:
|
||||||
cluster_rid = cluster_rels[0]
|
cluster_rid = cluster_rels[0]
|
||||||
@ -70,7 +68,7 @@ def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
|
|||||||
|
|
||||||
|
|
||||||
def peer_store(key, value, relation_name='cluster'):
|
def peer_store(key, value, relation_name='cluster'):
|
||||||
""" Store the key/value pair on the named peer relation relation_name """
|
"""Store the key/value pair on the named peer relation `relation_name`."""
|
||||||
cluster_rels = relation_ids(relation_name)
|
cluster_rels = relation_ids(relation_name)
|
||||||
if len(cluster_rels) > 0:
|
if len(cluster_rels) > 0:
|
||||||
cluster_rid = cluster_rels[0]
|
cluster_rid = cluster_rels[0]
|
||||||
@ -82,10 +80,10 @@ def peer_store(key, value, relation_name='cluster'):
|
|||||||
|
|
||||||
|
|
||||||
def peer_echo(includes=None):
|
def peer_echo(includes=None):
|
||||||
"""Echo filtered attributes back onto the same relation for storage
|
"""Echo filtered attributes back onto the same relation for storage.
|
||||||
|
|
||||||
Note that this helper must only be called within a peer relation
|
This is a requirement to use the peerstorage module - it needs to be called
|
||||||
changed hook
|
from the peer relation's changed hook.
|
||||||
"""
|
"""
|
||||||
rdata = relation_get()
|
rdata = relation_get()
|
||||||
echo_data = {}
|
echo_data = {}
|
||||||
|
@ -156,12 +156,15 @@ def hook_name():
|
|||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
"""A Juju charm config dictionary that can write itself to
|
"""A dictionary representation of the charm's config.yaml, with some
|
||||||
disk (as json) and track which values have changed since
|
extra features:
|
||||||
the previous hook invocation.
|
|
||||||
|
|
||||||
Do not instantiate this object directly - instead call
|
- See which values in the dictionary have changed since the previous hook.
|
||||||
``hookenv.config()``
|
- For values that have changed, see what the previous value was.
|
||||||
|
- Store arbitrary data for use in a later hook.
|
||||||
|
|
||||||
|
NOTE: Do not instantiate this object directly - instead call
|
||||||
|
``hookenv.config()``, which will return an instance of :class:`Config`.
|
||||||
|
|
||||||
Example usage::
|
Example usage::
|
||||||
|
|
||||||
@ -170,8 +173,8 @@ class Config(dict):
|
|||||||
>>> config = hookenv.config()
|
>>> config = hookenv.config()
|
||||||
>>> config['foo']
|
>>> config['foo']
|
||||||
'bar'
|
'bar'
|
||||||
|
>>> # store a new key/value for later use
|
||||||
>>> config['mykey'] = 'myval'
|
>>> config['mykey'] = 'myval'
|
||||||
>>> config.save()
|
|
||||||
|
|
||||||
|
|
||||||
>>> # user runs `juju set mycharm foo=baz`
|
>>> # user runs `juju set mycharm foo=baz`
|
||||||
@ -188,22 +191,34 @@ class Config(dict):
|
|||||||
>>> # keys/values that we add are preserved across hooks
|
>>> # keys/values that we add are preserved across hooks
|
||||||
>>> config['mykey']
|
>>> config['mykey']
|
||||||
'myval'
|
'myval'
|
||||||
>>> # don't forget to save at the end of hook!
|
|
||||||
>>> config.save()
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
CONFIG_FILE_NAME = '.juju-persistent-config'
|
CONFIG_FILE_NAME = '.juju-persistent-config'
|
||||||
|
|
||||||
def __init__(self, *args, **kw):
|
def __init__(self, *args, **kw):
|
||||||
super(Config, self).__init__(*args, **kw)
|
super(Config, self).__init__(*args, **kw)
|
||||||
|
self.implicit_save = True
|
||||||
self._prev_dict = None
|
self._prev_dict = None
|
||||||
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path):
|
||||||
self.load_previous()
|
self.load_previous()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""For regular dict lookups, check the current juju config first,
|
||||||
|
then the previous (saved) copy. This ensures that user-saved values
|
||||||
|
will be returned by a dict lookup.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return dict.__getitem__(self, key)
|
||||||
|
except KeyError:
|
||||||
|
return (self._prev_dict or {})[key]
|
||||||
|
|
||||||
def load_previous(self, path=None):
|
def load_previous(self, path=None):
|
||||||
"""Load previous copy of config from disk so that current values
|
"""Load previous copy of config from disk.
|
||||||
can be compared to previous values.
|
|
||||||
|
In normal usage you don't need to call this method directly - it
|
||||||
|
is called automatically at object initialization.
|
||||||
|
|
||||||
:param path:
|
:param path:
|
||||||
|
|
||||||
@ -218,8 +233,8 @@ class Config(dict):
|
|||||||
self._prev_dict = json.load(f)
|
self._prev_dict = json.load(f)
|
||||||
|
|
||||||
def changed(self, key):
|
def changed(self, key):
|
||||||
"""Return true if the value for this key has changed since
|
"""Return True if the current value for this key is different from
|
||||||
the last save.
|
the previous value.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._prev_dict is None:
|
if self._prev_dict is None:
|
||||||
@ -228,7 +243,7 @@ class Config(dict):
|
|||||||
|
|
||||||
def previous(self, key):
|
def previous(self, key):
|
||||||
"""Return previous value for this key, or None if there
|
"""Return previous value for this key, or None if there
|
||||||
is no "previous" value.
|
is no previous value.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._prev_dict:
|
if self._prev_dict:
|
||||||
@ -238,7 +253,13 @@ class Config(dict):
|
|||||||
def save(self):
|
def save(self):
|
||||||
"""Save this config to disk.
|
"""Save this config to disk.
|
||||||
|
|
||||||
Preserves items in _prev_dict that do not exist in self.
|
If the charm is using the :mod:`Services Framework <services.base>`
|
||||||
|
or :meth:'@hook <Hooks.hook>' decorator, this
|
||||||
|
is called automatically at the end of successful hook execution.
|
||||||
|
Otherwise, it should be called directly by user code.
|
||||||
|
|
||||||
|
To disable automatic saves, set ``implicit_save=False`` on this
|
||||||
|
instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._prev_dict:
|
if self._prev_dict:
|
||||||
@ -465,9 +486,10 @@ class Hooks(object):
|
|||||||
hooks.execute(sys.argv)
|
hooks.execute(sys.argv)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, config_save=True):
|
||||||
super(Hooks, self).__init__()
|
super(Hooks, self).__init__()
|
||||||
self._hooks = {}
|
self._hooks = {}
|
||||||
|
self._config_save = config_save
|
||||||
|
|
||||||
def register(self, name, function):
|
def register(self, name, function):
|
||||||
"""Register a hook"""
|
"""Register a hook"""
|
||||||
@ -478,6 +500,10 @@ class Hooks(object):
|
|||||||
hook_name = os.path.basename(args[0])
|
hook_name = os.path.basename(args[0])
|
||||||
if hook_name in self._hooks:
|
if hook_name in self._hooks:
|
||||||
self._hooks[hook_name]()
|
self._hooks[hook_name]()
|
||||||
|
if self._config_save:
|
||||||
|
cfg = config()
|
||||||
|
if cfg.implicit_save:
|
||||||
|
cfg.save()
|
||||||
else:
|
else:
|
||||||
raise UnregisteredHookError(hook_name)
|
raise UnregisteredHookError(hook_name)
|
||||||
|
|
||||||
|
@ -68,8 +68,8 @@ def service_available(service_name):
|
|||||||
"""Determine whether a system service is available"""
|
"""Determine whether a system service is available"""
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
|
subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError as e:
|
||||||
return False
|
return 'unrecognized service' not in e.output
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -209,10 +209,15 @@ def mounts():
|
|||||||
return system_mounts
|
return system_mounts
|
||||||
|
|
||||||
|
|
||||||
def file_hash(path):
|
def file_hash(path, hash_type='md5'):
|
||||||
"""Generate a md5 hash of the contents of 'path' or None if not found """
|
"""
|
||||||
|
Generate a hash checksum of the contents of 'path' or None if not found.
|
||||||
|
|
||||||
|
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
|
||||||
|
such as md5, sha1, sha256, sha512, etc.
|
||||||
|
"""
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
h = hashlib.md5()
|
h = getattr(hashlib, hash_type)()
|
||||||
with open(path, 'r') as source:
|
with open(path, 'r') as source:
|
||||||
h.update(source.read()) # IGNORE:E1101 - it does have update
|
h.update(source.read()) # IGNORE:E1101 - it does have update
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
@ -220,6 +225,26 @@ def file_hash(path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_hash(path, checksum, hash_type='md5'):
|
||||||
|
"""
|
||||||
|
Validate a file using a cryptographic checksum.
|
||||||
|
|
||||||
|
:param str checksum: Value of the checksum used to validate the file.
|
||||||
|
:param str hash_type: Hash algorithm used to generate `checksum`.
|
||||||
|
Can be any hash alrgorithm supported by :mod:`hashlib`,
|
||||||
|
such as md5, sha1, sha256, sha512, etc.
|
||||||
|
:raises ChecksumError: If the file fails the checksum
|
||||||
|
|
||||||
|
"""
|
||||||
|
actual_checksum = file_hash(path, hash_type)
|
||||||
|
if checksum != actual_checksum:
|
||||||
|
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
|
||||||
|
|
||||||
|
|
||||||
|
class ChecksumError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def restart_on_change(restart_map, stopstart=False):
|
def restart_on_change(restart_map, stopstart=False):
|
||||||
"""Restart services based on configuration files changing
|
"""Restart services based on configuration files changing
|
||||||
|
|
||||||
|
@ -118,6 +118,9 @@ class ServiceManager(object):
|
|||||||
else:
|
else:
|
||||||
self.provide_data()
|
self.provide_data()
|
||||||
self.reconfigure_services()
|
self.reconfigure_services()
|
||||||
|
cfg = hookenv.config()
|
||||||
|
if cfg.implicit_save:
|
||||||
|
cfg.save()
|
||||||
|
|
||||||
def provide_data(self):
|
def provide_data(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
from charmhelpers.core import hookenv
|
from charmhelpers.core import hookenv
|
||||||
from charmhelpers.core import templating
|
from charmhelpers.core import templating
|
||||||
|
|
||||||
@ -19,15 +21,21 @@ class RelationContext(dict):
|
|||||||
the `name` attribute that are complete will used to populate the dictionary
|
the `name` attribute that are complete will used to populate the dictionary
|
||||||
values (see `get_data`, below).
|
values (see `get_data`, below).
|
||||||
|
|
||||||
The generated context will be namespaced under the interface type, to prevent
|
The generated context will be namespaced under the relation :attr:`name`,
|
||||||
potential naming conflicts.
|
to prevent potential naming conflicts.
|
||||||
|
|
||||||
|
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
|
||||||
|
:param list additional_required_keys: Extend the list of :attr:`required_keys`
|
||||||
"""
|
"""
|
||||||
name = None
|
name = None
|
||||||
interface = None
|
interface = None
|
||||||
required_keys = []
|
required_keys = []
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, name=None, additional_required_keys=None):
|
||||||
super(RelationContext, self).__init__(*args, **kwargs)
|
if name is not None:
|
||||||
|
self.name = name
|
||||||
|
if additional_required_keys is not None:
|
||||||
|
self.required_keys.extend(additional_required_keys)
|
||||||
self.get_data()
|
self.get_data()
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
@ -101,9 +109,115 @@ class RelationContext(dict):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class MysqlRelation(RelationContext):
|
||||||
|
"""
|
||||||
|
Relation context for the `mysql` interface.
|
||||||
|
|
||||||
|
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
|
||||||
|
:param list additional_required_keys: Extend the list of :attr:`required_keys`
|
||||||
|
"""
|
||||||
|
name = 'db'
|
||||||
|
interface = 'mysql'
|
||||||
|
required_keys = ['host', 'user', 'password', 'database']
|
||||||
|
|
||||||
|
|
||||||
|
class HttpRelation(RelationContext):
|
||||||
|
"""
|
||||||
|
Relation context for the `http` interface.
|
||||||
|
|
||||||
|
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
|
||||||
|
:param list additional_required_keys: Extend the list of :attr:`required_keys`
|
||||||
|
"""
|
||||||
|
name = 'website'
|
||||||
|
interface = 'http'
|
||||||
|
required_keys = ['host', 'port']
|
||||||
|
|
||||||
|
def provide_data(self):
|
||||||
|
return {
|
||||||
|
'host': hookenv.unit_get('private-address'),
|
||||||
|
'port': 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredConfig(dict):
|
||||||
|
"""
|
||||||
|
Data context that loads config options with one or more mandatory options.
|
||||||
|
|
||||||
|
Once the required options have been changed from their default values, all
|
||||||
|
config options will be available, namespaced under `config` to prevent
|
||||||
|
potential naming conflicts (for example, between a config option and a
|
||||||
|
relation property).
|
||||||
|
|
||||||
|
:param list *args: List of options that must be changed from their default values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
self.required_options = args
|
||||||
|
self['config'] = hookenv.config()
|
||||||
|
with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
|
||||||
|
self.config = yaml.load(fp).get('options', {})
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
for option in self.required_options:
|
||||||
|
if option not in self['config']:
|
||||||
|
return False
|
||||||
|
current_value = self['config'][option]
|
||||||
|
default_value = self.config[option].get('default')
|
||||||
|
if current_value == default_value:
|
||||||
|
return False
|
||||||
|
if current_value in (None, '') and default_value in (None, ''):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self.__bool__()
|
||||||
|
|
||||||
|
|
||||||
|
class StoredContext(dict):
|
||||||
|
"""
|
||||||
|
A data context that always returns the data that it was first created with.
|
||||||
|
|
||||||
|
This is useful to do a one-time generation of things like passwords, that
|
||||||
|
will thereafter use the same value that was originally generated, instead
|
||||||
|
of generating a new value each time it is run.
|
||||||
|
"""
|
||||||
|
def __init__(self, file_name, config_data):
|
||||||
|
"""
|
||||||
|
If the file exists, populate `self` with the data from the file.
|
||||||
|
Otherwise, populate with the given data and persist it to the file.
|
||||||
|
"""
|
||||||
|
if os.path.exists(file_name):
|
||||||
|
self.update(self.read_context(file_name))
|
||||||
|
else:
|
||||||
|
self.store_context(file_name, config_data)
|
||||||
|
self.update(config_data)
|
||||||
|
|
||||||
|
def store_context(self, file_name, config_data):
|
||||||
|
if not os.path.isabs(file_name):
|
||||||
|
file_name = os.path.join(hookenv.charm_dir(), file_name)
|
||||||
|
with open(file_name, 'w') as file_stream:
|
||||||
|
os.fchmod(file_stream.fileno(), 0600)
|
||||||
|
yaml.dump(config_data, file_stream)
|
||||||
|
|
||||||
|
def read_context(self, file_name):
|
||||||
|
if not os.path.isabs(file_name):
|
||||||
|
file_name = os.path.join(hookenv.charm_dir(), file_name)
|
||||||
|
with open(file_name, 'r') as file_stream:
|
||||||
|
data = yaml.load(file_stream)
|
||||||
|
if not data:
|
||||||
|
raise OSError("%s is empty" % file_name)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class TemplateCallback(ManagerCallback):
|
class TemplateCallback(ManagerCallback):
|
||||||
"""
|
"""
|
||||||
Callback class that will render a template, for use as a ready action.
|
Callback class that will render a Jinja2 template, for use as a ready action.
|
||||||
|
|
||||||
|
:param str source: The template source file, relative to `$CHARM_DIR/templates`
|
||||||
|
:param str target: The target to write the rendered template to
|
||||||
|
:param str owner: The owner of the rendered file
|
||||||
|
:param str group: The group of the rendered file
|
||||||
|
:param int perms: The permissions of the rendered file
|
||||||
"""
|
"""
|
||||||
def __init__(self, source, target, owner='root', group='root', perms=0444):
|
def __init__(self, source, target, owner='root', group='root', perms=0444):
|
||||||
self.source = source
|
self.source = source
|
||||||
|
@ -208,7 +208,8 @@ def add_source(source, key=None):
|
|||||||
"""Add a package source to this system.
|
"""Add a package source to this system.
|
||||||
|
|
||||||
@param source: a URL or sources.list entry, as supported by
|
@param source: a URL or sources.list entry, as supported by
|
||||||
add-apt-repository(1). Examples:
|
add-apt-repository(1). Examples::
|
||||||
|
|
||||||
ppa:charmers/example
|
ppa:charmers/example
|
||||||
deb https://stub:key@private.example.com/ubuntu trusty main
|
deb https://stub:key@private.example.com/ubuntu trusty main
|
||||||
|
|
||||||
@ -311,22 +312,35 @@ def configure_sources(update=False,
|
|||||||
apt_update(fatal=True)
|
apt_update(fatal=True)
|
||||||
|
|
||||||
|
|
||||||
def install_remote(source):
|
def install_remote(source, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Install a file tree from a remote source
|
Install a file tree from a remote source
|
||||||
|
|
||||||
The specified source should be a url of the form:
|
The specified source should be a url of the form:
|
||||||
scheme://[host]/path[#[option=value][&...]]
|
scheme://[host]/path[#[option=value][&...]]
|
||||||
|
|
||||||
Schemes supported are based on this modules submodules
|
Schemes supported are based on this modules submodules.
|
||||||
Options supported are submodule-specific"""
|
Options supported are submodule-specific.
|
||||||
|
Additional arguments are passed through to the submodule.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
dest = install_remote('http://example.com/archive.tgz',
|
||||||
|
checksum='deadbeef',
|
||||||
|
hash_type='sha1')
|
||||||
|
|
||||||
|
This will download `archive.tgz`, validate it using SHA1 and, if
|
||||||
|
the file is ok, extract it and return the directory in which it
|
||||||
|
was extracted. If the checksum fails, it will raise
|
||||||
|
:class:`charmhelpers.core.host.ChecksumError`.
|
||||||
|
"""
|
||||||
# We ONLY check for True here because can_handle may return a string
|
# We ONLY check for True here because can_handle may return a string
|
||||||
# explaining why it can't handle a given source.
|
# explaining why it can't handle a given source.
|
||||||
handlers = [h for h in plugins() if h.can_handle(source) is True]
|
handlers = [h for h in plugins() if h.can_handle(source) is True]
|
||||||
installed_to = None
|
installed_to = None
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
try:
|
try:
|
||||||
installed_to = handler.install(source)
|
installed_to = handler.install(source, *args, **kwargs)
|
||||||
except UnhandledSource:
|
except UnhandledSource:
|
||||||
pass
|
pass
|
||||||
if not installed_to:
|
if not installed_to:
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import urllib2
|
import urllib2
|
||||||
|
from urllib import urlretrieve
|
||||||
import urlparse
|
import urlparse
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
BaseFetchHandler,
|
BaseFetchHandler,
|
||||||
@ -10,11 +12,19 @@ from charmhelpers.payload.archive import (
|
|||||||
get_archive_handler,
|
get_archive_handler,
|
||||||
extract,
|
extract,
|
||||||
)
|
)
|
||||||
from charmhelpers.core.host import mkdir
|
from charmhelpers.core.host import mkdir, check_hash
|
||||||
|
|
||||||
|
|
||||||
class ArchiveUrlFetchHandler(BaseFetchHandler):
|
class ArchiveUrlFetchHandler(BaseFetchHandler):
|
||||||
"""Handler for archives via generic URLs"""
|
"""
|
||||||
|
Handler to download archive files from arbitrary URLs.
|
||||||
|
|
||||||
|
Can fetch from http, https, ftp, and file URLs.
|
||||||
|
|
||||||
|
Can install either tarballs (.tar, .tgz, .tbz2, etc) or zip files.
|
||||||
|
|
||||||
|
Installs the contents of the archive in $CHARM_DIR/fetched/.
|
||||||
|
"""
|
||||||
def can_handle(self, source):
|
def can_handle(self, source):
|
||||||
url_parts = self.parse_url(source)
|
url_parts = self.parse_url(source)
|
||||||
if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
|
if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
|
||||||
@ -24,6 +34,12 @@ class ArchiveUrlFetchHandler(BaseFetchHandler):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def download(self, source, dest):
|
def download(self, source, dest):
|
||||||
|
"""
|
||||||
|
Download an archive file.
|
||||||
|
|
||||||
|
:param str source: URL pointing to an archive file.
|
||||||
|
:param str dest: Local path location to download archive file to.
|
||||||
|
"""
|
||||||
# propogate all exceptions
|
# propogate all exceptions
|
||||||
# URLError, OSError, etc
|
# URLError, OSError, etc
|
||||||
proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
|
proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
|
||||||
@ -48,7 +64,30 @@ class ArchiveUrlFetchHandler(BaseFetchHandler):
|
|||||||
os.unlink(dest)
|
os.unlink(dest)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def install(self, source):
|
# Mandatory file validation via Sha1 or MD5 hashing.
|
||||||
|
def download_and_validate(self, url, hashsum, validate="sha1"):
|
||||||
|
tempfile, headers = urlretrieve(url)
|
||||||
|
check_hash(tempfile, hashsum, validate)
|
||||||
|
return tempfile
|
||||||
|
|
||||||
|
def install(self, source, dest=None, checksum=None, hash_type='sha1'):
|
||||||
|
"""
|
||||||
|
Download and install an archive file, with optional checksum validation.
|
||||||
|
|
||||||
|
The checksum can also be given on the `source` URL's fragment.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
handler.install('http://example.com/file.tgz#sha1=deadbeef')
|
||||||
|
|
||||||
|
:param str source: URL pointing to an archive file.
|
||||||
|
:param str dest: Local destination path to install to. If not given,
|
||||||
|
installs to `$CHARM_DIR/archives/archive_file_name`.
|
||||||
|
:param str checksum: If given, validate the archive file after download.
|
||||||
|
:param str hash_type: Algorithm used to generate `checksum`.
|
||||||
|
Can be any hash alrgorithm supported by :mod:`hashlib`,
|
||||||
|
such as md5, sha1, sha256, sha512, etc.
|
||||||
|
|
||||||
|
"""
|
||||||
url_parts = self.parse_url(source)
|
url_parts = self.parse_url(source)
|
||||||
dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
|
dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
|
||||||
if not os.path.exists(dest_dir):
|
if not os.path.exists(dest_dir):
|
||||||
@ -60,4 +99,10 @@ class ArchiveUrlFetchHandler(BaseFetchHandler):
|
|||||||
raise UnhandledSource(e.reason)
|
raise UnhandledSource(e.reason)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UnhandledSource(e.strerror)
|
raise UnhandledSource(e.strerror)
|
||||||
return extract(dld_file)
|
options = urlparse.parse_qs(url_parts.fragment)
|
||||||
|
for key, value in options.items():
|
||||||
|
if key in hashlib.algorithms:
|
||||||
|
check_hash(dld_file, value, key)
|
||||||
|
if checksum:
|
||||||
|
check_hash(dld_file, checksum, hash_type)
|
||||||
|
return extract(dld_file, dest)
|
||||||
|
@ -24,25 +24,31 @@ class AmuletDeployment(object):
|
|||||||
"""Add services.
|
"""Add services.
|
||||||
|
|
||||||
Add services to the deployment where this_service is the local charm
|
Add services to the deployment where this_service is the local charm
|
||||||
that we're focused on testing and other_services are the other
|
that we're testing and other_services are the other services that
|
||||||
charms that come from the charm store.
|
are being used in the local amulet tests.
|
||||||
"""
|
"""
|
||||||
name, units = range(2)
|
if this_service['name'] != os.path.basename(os.getcwd()):
|
||||||
|
s = this_service['name']
|
||||||
if this_service[name] != os.path.basename(os.getcwd()):
|
|
||||||
s = this_service[name]
|
|
||||||
msg = "The charm's root directory name needs to be {}".format(s)
|
msg = "The charm's root directory name needs to be {}".format(s)
|
||||||
amulet.raise_status(amulet.FAIL, msg=msg)
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
||||||
|
|
||||||
self.d.add(this_service[name], units=this_service[units])
|
if 'units' not in this_service:
|
||||||
|
this_service['units'] = 1
|
||||||
|
|
||||||
|
self.d.add(this_service['name'], units=this_service['units'])
|
||||||
|
|
||||||
for svc in other_services:
|
for svc in other_services:
|
||||||
if self.series:
|
if 'location' in svc:
|
||||||
self.d.add(svc[name],
|
branch_location = svc['location']
|
||||||
charm='cs:{}/{}'.format(self.series, svc[name]),
|
elif self.series:
|
||||||
units=svc[units])
|
branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
|
||||||
else:
|
else:
|
||||||
self.d.add(svc[name], units=svc[units])
|
branch_location = None
|
||||||
|
|
||||||
|
if 'units' not in svc:
|
||||||
|
svc['units'] = 1
|
||||||
|
|
||||||
|
self.d.add(svc['name'], charm=branch_location, units=svc['units'])
|
||||||
|
|
||||||
def _add_relations(self, relations):
|
def _add_relations(self, relations):
|
||||||
"""Add all of the relations for the services."""
|
"""Add all of the relations for the services."""
|
||||||
@ -57,7 +63,7 @@ class AmuletDeployment(object):
|
|||||||
def _deploy(self):
|
def _deploy(self):
|
||||||
"""Deploy environment and wait for all hooks to finish executing."""
|
"""Deploy environment and wait for all hooks to finish executing."""
|
||||||
try:
|
try:
|
||||||
self.d.setup()
|
self.d.setup(timeout=900)
|
||||||
self.d.sentry.wait(timeout=900)
|
self.d.sentry.wait(timeout=900)
|
||||||
except amulet.helpers.TimeoutError:
|
except amulet.helpers.TimeoutError:
|
||||||
amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
|
amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
|
||||||
|
@ -10,32 +10,60 @@ class OpenStackAmuletDeployment(AmuletDeployment):
|
|||||||
that is specifically for use by OpenStack charms.
|
that is specifically for use by OpenStack charms.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, series=None, openstack=None, source=None):
|
def __init__(self, series=None, openstack=None, source=None, stable=True):
|
||||||
"""Initialize the deployment environment."""
|
"""Initialize the deployment environment."""
|
||||||
super(OpenStackAmuletDeployment, self).__init__(series)
|
super(OpenStackAmuletDeployment, self).__init__(series)
|
||||||
self.openstack = openstack
|
self.openstack = openstack
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.stable = stable
|
||||||
|
# Note(coreycb): this needs to be changed when new next branches come out.
|
||||||
|
self.current_next = "trusty"
|
||||||
|
|
||||||
|
def _determine_branch_locations(self, other_services):
|
||||||
|
"""Determine the branch locations for the other services.
|
||||||
|
|
||||||
|
Determine if the local branch being tested is derived from its
|
||||||
|
stable or next (dev) branch, and based on this, use the corresonding
|
||||||
|
stable or next branches for the other_services."""
|
||||||
|
base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
|
||||||
|
|
||||||
|
if self.stable:
|
||||||
|
for svc in other_services:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
for svc in other_services:
|
||||||
|
if svc['name'] in base_charms:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
temp = 'lp:~openstack-charmers/charms/{}/{}/next'
|
||||||
|
svc['location'] = temp.format(self.current_next,
|
||||||
|
svc['name'])
|
||||||
|
return other_services
|
||||||
|
|
||||||
def _add_services(self, this_service, other_services):
|
def _add_services(self, this_service, other_services):
|
||||||
"""Add services to the deployment and set openstack-origin."""
|
"""Add services to the deployment and set openstack-origin/source."""
|
||||||
|
other_services = self._determine_branch_locations(other_services)
|
||||||
|
|
||||||
super(OpenStackAmuletDeployment, self)._add_services(this_service,
|
super(OpenStackAmuletDeployment, self)._add_services(this_service,
|
||||||
other_services)
|
other_services)
|
||||||
name = 0
|
|
||||||
services = other_services
|
services = other_services
|
||||||
services.append(this_service)
|
services.append(this_service)
|
||||||
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
|
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
|
||||||
|
|
||||||
if self.openstack:
|
if self.openstack:
|
||||||
for svc in services:
|
for svc in services:
|
||||||
if svc[name] not in use_source:
|
if svc['name'] not in use_source:
|
||||||
config = {'openstack-origin': self.openstack}
|
config = {'openstack-origin': self.openstack}
|
||||||
self.d.configure(svc[name], config)
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
if self.source:
|
if self.source:
|
||||||
for svc in services:
|
for svc in services:
|
||||||
if svc[name] in use_source:
|
if svc['name'] in use_source:
|
||||||
config = {'source': self.source}
|
config = {'source': self.source}
|
||||||
self.d.configure(svc[name], config)
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
def _configure_services(self, configs):
|
def _configure_services(self, configs):
|
||||||
"""Configure all of the services."""
|
"""Configure all of the services."""
|
||||||
|
@ -187,15 +187,16 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
|
|
||||||
f = opener.open("http://download.cirros-cloud.net/version/released")
|
f = opener.open("http://download.cirros-cloud.net/version/released")
|
||||||
version = f.read().strip()
|
version = f.read().strip()
|
||||||
cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
|
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
|
||||||
|
local_path = os.path.join('tests', cirros_img)
|
||||||
|
|
||||||
if not os.path.exists(cirros_img):
|
if not os.path.exists(local_path):
|
||||||
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
|
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
|
||||||
version, cirros_img)
|
version, cirros_img)
|
||||||
opener.retrieve(cirros_url, cirros_img)
|
opener.retrieve(cirros_url, local_path)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
with open(cirros_img) as f:
|
with open(local_path) as f:
|
||||||
image = glance.images.create(name=image_name, is_public=True,
|
image = glance.images.create(name=image_name, is_public=True,
|
||||||
disk_format='qcow2',
|
disk_format='qcow2',
|
||||||
container_format='bare', data=f)
|
container_format='bare', data=f)
|
||||||
|
Loading…
Reference in New Issue
Block a user