Dual Stack VIPs
Enable dual stack IPv4 and IPv6 VIPs on the same interface. HAProxy always listens on both IPv4 and IPv6 allowing connectivity on either protocol. Update edge cases for is_ssl_cert_master for Bug #1709356. Update amulet tests for keystoneauth1 tests. charm-helpers sync for HAProxy template changes. Closes-Bug: #1709356 Change-Id: I401071fcdd66252f389475d45e8136fc68c474f1
This commit is contained in:
parent
1b395d09b2
commit
1328ce5880
@ -14,6 +14,11 @@
|
|||||||
|
|
||||||
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
||||||
# only standard libraries.
|
# only standard libraries.
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -34,3 +39,59 @@ except ImportError:
|
|||||||
else:
|
else:
|
||||||
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
||||||
import yaml # flake8: noqa
|
import yaml # flake8: noqa
|
||||||
|
|
||||||
|
|
||||||
|
# Holds a list of mapping of mangled function names that have been deprecated
|
||||||
|
# using the @deprecate decorator below. This is so that the warning is only
|
||||||
|
# printed once for each usage of the function.
|
||||||
|
__deprecated_functions = {}
|
||||||
|
|
||||||
|
|
||||||
|
def deprecate(warning, date=None, log=None):
|
||||||
|
"""Add a deprecation warning the first time the function is used.
|
||||||
|
The date, which is a string in semi-ISO8660 format indicate the year-month
|
||||||
|
that the function is officially going to be removed.
|
||||||
|
|
||||||
|
usage:
|
||||||
|
|
||||||
|
@deprecate('use core/fetch/add_source() instead', '2017-04')
|
||||||
|
def contributed_add_source_thing(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
And it then prints to the log ONCE that the function is deprecated.
|
||||||
|
The reason for passing the logging function (log) is so that hookenv.log
|
||||||
|
can be used for a charm if needed.
|
||||||
|
|
||||||
|
:param warning: String to indicat where it has moved ot.
|
||||||
|
:param date: optional sting, in YYYY-MM format to indicate when the
|
||||||
|
function will definitely (probably) be removed.
|
||||||
|
:param log: The log function to call to log. If not, logs to stdout
|
||||||
|
"""
|
||||||
|
def wrap(f):
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapped_f(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
module = inspect.getmodule(f)
|
||||||
|
file = inspect.getsourcefile(f)
|
||||||
|
lines = inspect.getsourcelines(f)
|
||||||
|
f_name = "{}-{}-{}..{}-{}".format(
|
||||||
|
module.__name__, file, lines[0], lines[-1], f.__name__)
|
||||||
|
except (IOError, TypeError):
|
||||||
|
# assume it was local, so just use the name of the function
|
||||||
|
f_name = f.__name__
|
||||||
|
if f_name not in __deprecated_functions:
|
||||||
|
__deprecated_functions[f_name] = True
|
||||||
|
s = "DEPRECATION WARNING: Function {} is being removed".format(
|
||||||
|
f.__name__)
|
||||||
|
if date:
|
||||||
|
s = "{} on/around {}".format(s, date)
|
||||||
|
if warning:
|
||||||
|
s = "{} : {}".format(s, warning)
|
||||||
|
if log:
|
||||||
|
log(s)
|
||||||
|
else:
|
||||||
|
print(s)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapped_f
|
||||||
|
return wrap
|
||||||
|
@ -125,7 +125,7 @@ class CheckException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class Check(object):
|
class Check(object):
|
||||||
shortname_re = '[A-Za-z0-9-_]+$'
|
shortname_re = '[A-Za-z0-9-_.]+$'
|
||||||
service_template = ("""
|
service_template = ("""
|
||||||
#---------------------------------------------------
|
#---------------------------------------------------
|
||||||
# This file is Juju managed
|
# This file is Juju managed
|
||||||
@ -193,6 +193,13 @@ define service {{
|
|||||||
nrpe_check_file = self._get_check_filename()
|
nrpe_check_file = self._get_check_filename()
|
||||||
with open(nrpe_check_file, 'w') as nrpe_check_config:
|
with open(nrpe_check_file, 'w') as nrpe_check_config:
|
||||||
nrpe_check_config.write("# check {}\n".format(self.shortname))
|
nrpe_check_config.write("# check {}\n".format(self.shortname))
|
||||||
|
if nagios_servicegroups:
|
||||||
|
nrpe_check_config.write(
|
||||||
|
"# The following header was added automatically by juju\n")
|
||||||
|
nrpe_check_config.write(
|
||||||
|
"# Modifying it will affect nagios monitoring and alerting\n")
|
||||||
|
nrpe_check_config.write(
|
||||||
|
"# servicegroups: {}\n".format(nagios_servicegroups))
|
||||||
nrpe_check_config.write("command[{}]={}\n".format(
|
nrpe_check_config.write("command[{}]={}\n".format(
|
||||||
self.command, self.check_cmd))
|
self.command, self.check_cmd))
|
||||||
|
|
||||||
|
@ -243,11 +243,13 @@ def is_ipv6_disabled():
|
|||||||
try:
|
try:
|
||||||
result = subprocess.check_output(
|
result = subprocess.check_output(
|
||||||
['sysctl', 'net.ipv6.conf.all.disable_ipv6'],
|
['sysctl', 'net.ipv6.conf.all.disable_ipv6'],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT,
|
||||||
return "net.ipv6.conf.all.disable_ipv6 = 1" in result
|
universal_newlines=True)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return "net.ipv6.conf.all.disable_ipv6 = 1" in result
|
||||||
|
|
||||||
|
|
||||||
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
|
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
|
||||||
fatal=True, exc_list=None):
|
fatal=True, exc_list=None):
|
||||||
|
@ -25,9 +25,12 @@ import urlparse
|
|||||||
import cinderclient.v1.client as cinder_client
|
import cinderclient.v1.client as cinder_client
|
||||||
import glanceclient.v1.client as glance_client
|
import glanceclient.v1.client as glance_client
|
||||||
import heatclient.v1.client as heat_client
|
import heatclient.v1.client as heat_client
|
||||||
import keystoneclient.v2_0 as keystone_client
|
from keystoneclient.v2_0 import client as keystone_client
|
||||||
from keystoneclient.auth.identity import v3 as keystone_id_v3
|
from keystoneauth1.identity import (
|
||||||
from keystoneclient import session as keystone_session
|
v3,
|
||||||
|
v2,
|
||||||
|
)
|
||||||
|
from keystoneauth1 import session as keystone_session
|
||||||
from keystoneclient.v3 import client as keystone_client_v3
|
from keystoneclient.v3 import client as keystone_client_v3
|
||||||
from novaclient import exceptions
|
from novaclient import exceptions
|
||||||
|
|
||||||
@ -368,12 +371,20 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
port)
|
port)
|
||||||
if not api_version or api_version == 2:
|
if not api_version or api_version == 2:
|
||||||
ep = base_ep + "/v2.0"
|
ep = base_ep + "/v2.0"
|
||||||
return keystone_client.Client(username=username, password=password,
|
auth = v2.Password(
|
||||||
tenant_name=project_name,
|
username=username,
|
||||||
auth_url=ep)
|
password=password,
|
||||||
|
tenant_name=project_name,
|
||||||
|
auth_url=ep
|
||||||
|
)
|
||||||
|
sess = keystone_session.Session(auth=auth)
|
||||||
|
client = keystone_client.Client(session=sess)
|
||||||
|
# This populates the client.service_catalog
|
||||||
|
client.auth_ref = auth.get_access(sess)
|
||||||
|
return client
|
||||||
else:
|
else:
|
||||||
ep = base_ep + "/v3"
|
ep = base_ep + "/v3"
|
||||||
auth = keystone_id_v3.Password(
|
auth = v3.Password(
|
||||||
user_domain_name=user_domain_name,
|
user_domain_name=user_domain_name,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
@ -382,36 +393,45 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
auth_url=ep
|
auth_url=ep
|
||||||
)
|
)
|
||||||
return keystone_client_v3.Client(
|
sess = keystone_session.Session(auth=auth)
|
||||||
session=keystone_session.Session(auth=auth)
|
client = keystone_client_v3.Client(session=sess)
|
||||||
)
|
# This populates the client.service_catalog
|
||||||
|
client.auth_ref = auth.get_access(sess)
|
||||||
|
return client
|
||||||
|
|
||||||
def authenticate_keystone_admin(self, keystone_sentry, user, password,
|
def authenticate_keystone_admin(self, keystone_sentry, user, password,
|
||||||
tenant=None, api_version=None,
|
tenant=None, api_version=None,
|
||||||
keystone_ip=None):
|
keystone_ip=None, user_domain_name=None,
|
||||||
|
project_domain_name=None,
|
||||||
|
project_name=None):
|
||||||
"""Authenticates admin user with the keystone admin endpoint."""
|
"""Authenticates admin user with the keystone admin endpoint."""
|
||||||
self.log.debug('Authenticating keystone admin...')
|
self.log.debug('Authenticating keystone admin...')
|
||||||
if not keystone_ip:
|
if not keystone_ip:
|
||||||
keystone_ip = keystone_sentry.info['public-address']
|
keystone_ip = keystone_sentry.info['public-address']
|
||||||
|
|
||||||
user_domain_name = None
|
# To support backward compatibility usage of this function
|
||||||
domain_name = None
|
if not project_name:
|
||||||
if api_version == 3:
|
project_name = tenant
|
||||||
|
if api_version == 3 and not user_domain_name:
|
||||||
user_domain_name = 'admin_domain'
|
user_domain_name = 'admin_domain'
|
||||||
domain_name = user_domain_name
|
if api_version == 3 and not project_domain_name:
|
||||||
|
project_domain_name = 'admin_domain'
|
||||||
|
if api_version == 3 and not project_name:
|
||||||
|
project_name = 'admin'
|
||||||
|
|
||||||
return self.authenticate_keystone(keystone_ip, user, password,
|
return self.authenticate_keystone(
|
||||||
project_name=tenant,
|
keystone_ip, user, password,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
user_domain_name=user_domain_name,
|
user_domain_name=user_domain_name,
|
||||||
domain_name=domain_name,
|
project_domain_name=project_domain_name,
|
||||||
admin_port=True)
|
project_name=project_name,
|
||||||
|
admin_port=True)
|
||||||
|
|
||||||
def authenticate_keystone_user(self, keystone, user, password, tenant):
|
def authenticate_keystone_user(self, keystone, user, password, tenant):
|
||||||
"""Authenticates a regular user with the keystone public endpoint."""
|
"""Authenticates a regular user with the keystone public endpoint."""
|
||||||
self.log.debug('Authenticating keystone user ({})...'.format(user))
|
self.log.debug('Authenticating keystone user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
keystone_ip = urlparse.urlparse(ep).hostname
|
keystone_ip = urlparse.urlparse(ep).hostname
|
||||||
|
|
||||||
return self.authenticate_keystone(keystone_ip, user, password,
|
return self.authenticate_keystone(keystone_ip, user, password,
|
||||||
@ -421,22 +441,32 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
"""Authenticates admin user with glance."""
|
"""Authenticates admin user with glance."""
|
||||||
self.log.debug('Authenticating glance admin...')
|
self.log.debug('Authenticating glance admin...')
|
||||||
ep = keystone.service_catalog.url_for(service_type='image',
|
ep = keystone.service_catalog.url_for(service_type='image',
|
||||||
endpoint_type='adminURL')
|
interface='adminURL')
|
||||||
return glance_client.Client(ep, token=keystone.auth_token)
|
if keystone.session:
|
||||||
|
return glance_client.Client(ep, session=keystone.session)
|
||||||
|
else:
|
||||||
|
return glance_client.Client(ep, token=keystone.auth_token)
|
||||||
|
|
||||||
def authenticate_heat_admin(self, keystone):
|
def authenticate_heat_admin(self, keystone):
|
||||||
"""Authenticates the admin user with heat."""
|
"""Authenticates the admin user with heat."""
|
||||||
self.log.debug('Authenticating heat admin...')
|
self.log.debug('Authenticating heat admin...')
|
||||||
ep = keystone.service_catalog.url_for(service_type='orchestration',
|
ep = keystone.service_catalog.url_for(service_type='orchestration',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
return heat_client.Client(endpoint=ep, token=keystone.auth_token)
|
if keystone.session:
|
||||||
|
return heat_client.Client(endpoint=ep, session=keystone.session)
|
||||||
|
else:
|
||||||
|
return heat_client.Client(endpoint=ep, token=keystone.auth_token)
|
||||||
|
|
||||||
def authenticate_nova_user(self, keystone, user, password, tenant):
|
def authenticate_nova_user(self, keystone, user, password, tenant):
|
||||||
"""Authenticates a regular user with nova-api."""
|
"""Authenticates a regular user with nova-api."""
|
||||||
self.log.debug('Authenticating nova user ({})...'.format(user))
|
self.log.debug('Authenticating nova user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
if novaclient.__version__[0] >= "7":
|
if keystone.session:
|
||||||
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
|
session=keystone.session,
|
||||||
|
auth_url=ep)
|
||||||
|
elif novaclient.__version__[0] >= "7":
|
||||||
return nova_client.Client(NOVA_CLIENT_VERSION,
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
username=user, password=password,
|
username=user, password=password,
|
||||||
project_name=tenant, auth_url=ep)
|
project_name=tenant, auth_url=ep)
|
||||||
@ -449,12 +479,15 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
"""Authenticates a regular user with swift api."""
|
"""Authenticates a regular user with swift api."""
|
||||||
self.log.debug('Authenticating swift user ({})...'.format(user))
|
self.log.debug('Authenticating swift user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
return swiftclient.Connection(authurl=ep,
|
if keystone.session:
|
||||||
user=user,
|
return swiftclient.Connection(session=keystone.session)
|
||||||
key=password,
|
else:
|
||||||
tenant_name=tenant,
|
return swiftclient.Connection(authurl=ep,
|
||||||
auth_version='2.0')
|
user=user,
|
||||||
|
key=password,
|
||||||
|
tenant_name=tenant,
|
||||||
|
auth_version='2.0')
|
||||||
|
|
||||||
def create_flavor(self, nova, name, ram, vcpus, disk, flavorid="auto",
|
def create_flavor(self, nova, name, ram, vcpus, disk, flavorid="auto",
|
||||||
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
|
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
|
||||||
|
@ -41,9 +41,9 @@ from charmhelpers.core.hookenv import (
|
|||||||
charm_name,
|
charm_name,
|
||||||
DEBUG,
|
DEBUG,
|
||||||
INFO,
|
INFO,
|
||||||
WARNING,
|
|
||||||
ERROR,
|
ERROR,
|
||||||
status_set,
|
status_set,
|
||||||
|
network_get_primary_address
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.sysctl import create as sysctl_create
|
from charmhelpers.core.sysctl import create as sysctl_create
|
||||||
@ -80,6 +80,9 @@ from charmhelpers.contrib.openstack.neutron import (
|
|||||||
from charmhelpers.contrib.openstack.ip import (
|
from charmhelpers.contrib.openstack.ip import (
|
||||||
resolve_address,
|
resolve_address,
|
||||||
INTERNAL,
|
INTERNAL,
|
||||||
|
ADMIN,
|
||||||
|
PUBLIC,
|
||||||
|
ADDRESS_MAP,
|
||||||
)
|
)
|
||||||
from charmhelpers.contrib.network.ip import (
|
from charmhelpers.contrib.network.ip import (
|
||||||
get_address_in_network,
|
get_address_in_network,
|
||||||
@ -87,7 +90,6 @@ from charmhelpers.contrib.network.ip import (
|
|||||||
get_ipv6_addr,
|
get_ipv6_addr,
|
||||||
get_netmask_for_address,
|
get_netmask_for_address,
|
||||||
format_ipv6_addr,
|
format_ipv6_addr,
|
||||||
is_address_in_network,
|
|
||||||
is_bridge_member,
|
is_bridge_member,
|
||||||
is_ipv6_disabled,
|
is_ipv6_disabled,
|
||||||
)
|
)
|
||||||
@ -97,6 +99,7 @@ from charmhelpers.contrib.openstack.utils import (
|
|||||||
git_determine_usr_bin,
|
git_determine_usr_bin,
|
||||||
git_determine_python_path,
|
git_determine_python_path,
|
||||||
enable_memcache,
|
enable_memcache,
|
||||||
|
snap_install_requested,
|
||||||
)
|
)
|
||||||
from charmhelpers.core.unitdata import kv
|
from charmhelpers.core.unitdata import kv
|
||||||
|
|
||||||
@ -244,6 +247,11 @@ class SharedDBContext(OSContextGenerator):
|
|||||||
'database_password': rdata.get(password_setting),
|
'database_password': rdata.get(password_setting),
|
||||||
'database_type': 'mysql'
|
'database_type': 'mysql'
|
||||||
}
|
}
|
||||||
|
# Note(coreycb): We can drop mysql+pymysql if we want when the
|
||||||
|
# following review lands, though it seems mysql+pymysql would
|
||||||
|
# be preferred. https://review.openstack.org/#/c/462190/
|
||||||
|
if snap_install_requested():
|
||||||
|
ctxt['database_type'] = 'mysql+pymysql'
|
||||||
if self.context_complete(ctxt):
|
if self.context_complete(ctxt):
|
||||||
db_ssl(rdata, ctxt, self.ssl_dir)
|
db_ssl(rdata, ctxt, self.ssl_dir)
|
||||||
return ctxt
|
return ctxt
|
||||||
@ -510,6 +518,10 @@ class CephContext(OSContextGenerator):
|
|||||||
ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
|
ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
|
||||||
if not ctxt.get('key'):
|
if not ctxt.get('key'):
|
||||||
ctxt['key'] = relation_get('key', rid=rid, unit=unit)
|
ctxt['key'] = relation_get('key', rid=rid, unit=unit)
|
||||||
|
if not ctxt.get('rbd_features'):
|
||||||
|
default_features = relation_get('rbd-features', rid=rid, unit=unit)
|
||||||
|
if default_features is not None:
|
||||||
|
ctxt['rbd_features'] = default_features
|
||||||
|
|
||||||
ceph_addrs = relation_get('ceph-public-address', rid=rid,
|
ceph_addrs = relation_get('ceph-public-address', rid=rid,
|
||||||
unit=unit)
|
unit=unit)
|
||||||
@ -610,7 +622,6 @@ class HAProxyContext(OSContextGenerator):
|
|||||||
ctxt['haproxy_connect_timeout'] = config('haproxy-connect-timeout')
|
ctxt['haproxy_connect_timeout'] = config('haproxy-connect-timeout')
|
||||||
|
|
||||||
if config('prefer-ipv6'):
|
if config('prefer-ipv6'):
|
||||||
ctxt['ipv6'] = True
|
|
||||||
ctxt['local_host'] = 'ip6-localhost'
|
ctxt['local_host'] = 'ip6-localhost'
|
||||||
ctxt['haproxy_host'] = '::'
|
ctxt['haproxy_host'] = '::'
|
||||||
else:
|
else:
|
||||||
@ -726,11 +737,17 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
return sorted(list(set(cns)))
|
return sorted(list(set(cns)))
|
||||||
|
|
||||||
def get_network_addresses(self):
|
def get_network_addresses(self):
|
||||||
"""For each network configured, return corresponding address and vip
|
"""For each network configured, return corresponding address and
|
||||||
(if available).
|
hostnamr or vip (if available).
|
||||||
|
|
||||||
Returns a list of tuples of the form:
|
Returns a list of tuples of the form:
|
||||||
|
|
||||||
|
[(address_in_net_a, hostname_in_net_a),
|
||||||
|
(address_in_net_b, hostname_in_net_b),
|
||||||
|
...]
|
||||||
|
|
||||||
|
or, if no hostnames(s) available:
|
||||||
|
|
||||||
[(address_in_net_a, vip_in_net_a),
|
[(address_in_net_a, vip_in_net_a),
|
||||||
(address_in_net_b, vip_in_net_b),
|
(address_in_net_b, vip_in_net_b),
|
||||||
...]
|
...]
|
||||||
@ -742,32 +759,27 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
...]
|
...]
|
||||||
"""
|
"""
|
||||||
addresses = []
|
addresses = []
|
||||||
if config('vip'):
|
for net_type in [INTERNAL, ADMIN, PUBLIC]:
|
||||||
vips = config('vip').split()
|
net_config = config(ADDRESS_MAP[net_type]['config'])
|
||||||
else:
|
# NOTE(jamespage): Fallback must always be private address
|
||||||
vips = []
|
# as this is used to bind services on the
|
||||||
|
# local unit.
|
||||||
for net_type in ['os-internal-network', 'os-admin-network',
|
fallback = unit_get("private-address")
|
||||||
'os-public-network']:
|
if net_config:
|
||||||
addr = get_address_in_network(config(net_type),
|
addr = get_address_in_network(net_config,
|
||||||
unit_get('private-address'))
|
fallback)
|
||||||
if len(vips) > 1 and is_clustered():
|
|
||||||
if not config(net_type):
|
|
||||||
log("Multiple networks configured but net_type "
|
|
||||||
"is None (%s)." % net_type, level=WARNING)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for vip in vips:
|
|
||||||
if is_address_in_network(config(net_type), vip):
|
|
||||||
addresses.append((addr, vip))
|
|
||||||
break
|
|
||||||
|
|
||||||
elif is_clustered() and config('vip'):
|
|
||||||
addresses.append((addr, config('vip')))
|
|
||||||
else:
|
else:
|
||||||
addresses.append((addr, addr))
|
try:
|
||||||
|
addr = network_get_primary_address(
|
||||||
|
ADDRESS_MAP[net_type]['binding']
|
||||||
|
)
|
||||||
|
except NotImplementedError:
|
||||||
|
addr = fallback
|
||||||
|
|
||||||
return sorted(addresses)
|
endpoint = resolve_address(net_type)
|
||||||
|
addresses.append((addr, endpoint))
|
||||||
|
|
||||||
|
return sorted(set(addresses))
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
if isinstance(self.external_ports, six.string_types):
|
if isinstance(self.external_ports, six.string_types):
|
||||||
@ -794,7 +806,7 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
self.configure_cert(cn)
|
self.configure_cert(cn)
|
||||||
|
|
||||||
addresses = self.get_network_addresses()
|
addresses = self.get_network_addresses()
|
||||||
for address, endpoint in sorted(set(addresses)):
|
for address, endpoint in 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,
|
||||||
singlenode_mode=True)
|
singlenode_mode=True)
|
||||||
@ -1397,14 +1409,38 @@ class NeutronAPIContext(OSContextGenerator):
|
|||||||
'rel_key': 'dns-domain',
|
'rel_key': 'dns-domain',
|
||||||
'default': None,
|
'default': None,
|
||||||
},
|
},
|
||||||
|
'polling_interval': {
|
||||||
|
'rel_key': 'polling-interval',
|
||||||
|
'default': 2,
|
||||||
|
},
|
||||||
|
'rpc_response_timeout': {
|
||||||
|
'rel_key': 'rpc-response-timeout',
|
||||||
|
'default': 60,
|
||||||
|
},
|
||||||
|
'report_interval': {
|
||||||
|
'rel_key': 'report-interval',
|
||||||
|
'default': 30,
|
||||||
|
},
|
||||||
|
'enable_qos': {
|
||||||
|
'rel_key': 'enable-qos',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
ctxt = self.get_neutron_options({})
|
ctxt = self.get_neutron_options({})
|
||||||
for rid in relation_ids('neutron-plugin-api'):
|
for rid in relation_ids('neutron-plugin-api'):
|
||||||
for unit in related_units(rid):
|
for unit in related_units(rid):
|
||||||
rdata = relation_get(rid=rid, unit=unit)
|
rdata = relation_get(rid=rid, unit=unit)
|
||||||
|
# The l2-population key is used by the context as a way of
|
||||||
|
# checking if the api service on the other end is sending data
|
||||||
|
# in a recent format.
|
||||||
if 'l2-population' in rdata:
|
if 'l2-population' in rdata:
|
||||||
ctxt.update(self.get_neutron_options(rdata))
|
ctxt.update(self.get_neutron_options(rdata))
|
||||||
|
|
||||||
|
if ctxt['enable_qos']:
|
||||||
|
ctxt['extension_drivers'] = 'qos'
|
||||||
|
else:
|
||||||
|
ctxt['extension_drivers'] = ''
|
||||||
|
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
def get_neutron_options(self, rdata):
|
def get_neutron_options(self, rdata):
|
||||||
|
@ -29,7 +29,7 @@ def get_api_suffix(api_version):
|
|||||||
@returns the api suffix formatted according to the given api
|
@returns the api suffix formatted according to the given api
|
||||||
version
|
version
|
||||||
"""
|
"""
|
||||||
return 'v2.0' if api_version in (2, "2.0") else 'v3'
|
return 'v2.0' if api_version in (2, "2", "2.0") else 'v3'
|
||||||
|
|
||||||
|
|
||||||
def format_endpoint(schema, addr, port, api_version):
|
def format_endpoint(schema, addr, port, api_version):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# [ WARNING ]
|
# [ WARNING ]
|
||||||
# cinder configuration file maintained by Juju
|
# ceph configuration file maintained by Juju
|
||||||
# local changes may be overwritten.
|
# local changes may be overwritten.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
[global]
|
[global]
|
||||||
@ -12,6 +12,9 @@ mon host = {{ mon_hosts }}
|
|||||||
log to syslog = {{ use_syslog }}
|
log to syslog = {{ use_syslog }}
|
||||||
err to syslog = {{ use_syslog }}
|
err to syslog = {{ use_syslog }}
|
||||||
clog to syslog = {{ use_syslog }}
|
clog to syslog = {{ use_syslog }}
|
||||||
|
{% if rbd_features %}
|
||||||
|
rbd default features = {{ rbd_features }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
{% if rbd_client_cache_settings -%}
|
{% if rbd_client_cache_settings -%}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
global
|
global
|
||||||
log {{ local_host }} local0
|
log /var/lib/haproxy/dev/log local0
|
||||||
log {{ local_host }} local1 notice
|
log /var/lib/haproxy/dev/log local1 notice
|
||||||
maxconn 20000
|
maxconn 20000
|
||||||
user haproxy
|
user haproxy
|
||||||
group haproxy
|
group haproxy
|
||||||
@ -48,9 +48,7 @@ listen stats
|
|||||||
{% for service, ports in service_ports.items() -%}
|
{% for service, ports in service_ports.items() -%}
|
||||||
frontend tcp-in_{{ service }}
|
frontend tcp-in_{{ service }}
|
||||||
bind *:{{ ports[0] }}
|
bind *:{{ ports[0] }}
|
||||||
{% if ipv6 -%}
|
|
||||||
bind :::{{ ports[0] }}
|
bind :::{{ ports[0] }}
|
||||||
{% endif -%}
|
|
||||||
{% for frontend in frontends -%}
|
{% for frontend in frontends -%}
|
||||||
acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
|
acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
|
||||||
use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
|
use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
{% if transport_url -%}
|
||||||
|
[oslo_messaging_notifications]
|
||||||
|
driver = messagingv2
|
||||||
|
transport_url = {{ transport_url }}
|
||||||
|
{% if notification_topics -%}
|
||||||
|
topics = {{ notification_topics }}
|
||||||
|
{% endif -%}
|
||||||
|
{% endif -%}
|
@ -26,11 +26,12 @@ import functools
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import tempfile
|
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from charmhelpers import deprecate
|
||||||
|
|
||||||
from charmhelpers.contrib.network import ip
|
from charmhelpers.contrib.network import ip
|
||||||
|
|
||||||
from charmhelpers.core import unitdata
|
from charmhelpers.core import unitdata
|
||||||
@ -41,7 +42,6 @@ from charmhelpers.core.hookenv import (
|
|||||||
config,
|
config,
|
||||||
log as juju_log,
|
log as juju_log,
|
||||||
charm_dir,
|
charm_dir,
|
||||||
DEBUG,
|
|
||||||
INFO,
|
INFO,
|
||||||
ERROR,
|
ERROR,
|
||||||
related_units,
|
related_units,
|
||||||
@ -51,6 +51,7 @@ from charmhelpers.core.hookenv import (
|
|||||||
status_set,
|
status_set,
|
||||||
hook_name,
|
hook_name,
|
||||||
application_version_set,
|
application_version_set,
|
||||||
|
cached,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.strutils import BasicStringComparator
|
from charmhelpers.core.strutils import BasicStringComparator
|
||||||
@ -82,11 +83,21 @@ from charmhelpers.core.host import (
|
|||||||
restart_on_change_helper,
|
restart_on_change_helper,
|
||||||
)
|
)
|
||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
apt_install,
|
|
||||||
apt_cache,
|
apt_cache,
|
||||||
install_remote,
|
install_remote,
|
||||||
|
import_key as fetch_import_key,
|
||||||
|
add_source as fetch_add_source,
|
||||||
|
SourceConfigError,
|
||||||
|
GPGKeyError,
|
||||||
get_upstream_version
|
get_upstream_version
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from charmhelpers.fetch.snap import (
|
||||||
|
snap_install,
|
||||||
|
snap_refresh,
|
||||||
|
SNAP_CHANNELS,
|
||||||
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
||||||
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
||||||
from charmhelpers.contrib.openstack.exceptions import OSContextError
|
from charmhelpers.contrib.openstack.exceptions import OSContextError
|
||||||
@ -175,7 +186,7 @@ SWIFT_CODENAMES = OrderedDict([
|
|||||||
('ocata',
|
('ocata',
|
||||||
['2.11.0', '2.12.0', '2.13.0']),
|
['2.11.0', '2.12.0', '2.13.0']),
|
||||||
('pike',
|
('pike',
|
||||||
['2.13.0']),
|
['2.13.0', '2.15.0']),
|
||||||
])
|
])
|
||||||
|
|
||||||
# >= Liberty version->codename mapping
|
# >= Liberty version->codename mapping
|
||||||
@ -324,8 +335,10 @@ def get_os_codename_install_source(src):
|
|||||||
return ca_rel
|
return ca_rel
|
||||||
|
|
||||||
# Best guess match based on deb string provided
|
# Best guess match based on deb string provided
|
||||||
if src.startswith('deb') or src.startswith('ppa'):
|
if (src.startswith('deb') or
|
||||||
for k, v in six.iteritems(OPENSTACK_CODENAMES):
|
src.startswith('ppa') or
|
||||||
|
src.startswith('snap')):
|
||||||
|
for v in OPENSTACK_CODENAMES.values():
|
||||||
if v in src:
|
if v in src:
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@ -394,6 +407,19 @@ def get_swift_codename(version):
|
|||||||
|
|
||||||
def get_os_codename_package(package, fatal=True):
|
def get_os_codename_package(package, fatal=True):
|
||||||
'''Derive OpenStack release codename from an installed package.'''
|
'''Derive OpenStack release codename from an installed package.'''
|
||||||
|
|
||||||
|
if snap_install_requested():
|
||||||
|
cmd = ['snap', 'list', package]
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(cmd)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return None
|
||||||
|
lines = out.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if package in line:
|
||||||
|
# Second item in list is Version
|
||||||
|
return line.split()[1]
|
||||||
|
|
||||||
import apt_pkg as apt
|
import apt_pkg as apt
|
||||||
|
|
||||||
cache = apt_cache()
|
cache = apt_cache()
|
||||||
@ -469,13 +495,14 @@ def get_os_version_package(pkg, fatal=True):
|
|||||||
# error_out(e)
|
# error_out(e)
|
||||||
|
|
||||||
|
|
||||||
os_rel = None
|
# Module local cache variable for the os_release.
|
||||||
|
_os_rel = None
|
||||||
|
|
||||||
|
|
||||||
def reset_os_release():
|
def reset_os_release():
|
||||||
'''Unset the cached os_release version'''
|
'''Unset the cached os_release version'''
|
||||||
global os_rel
|
global _os_rel
|
||||||
os_rel = None
|
_os_rel = None
|
||||||
|
|
||||||
|
|
||||||
def os_release(package, base='essex', reset_cache=False):
|
def os_release(package, base='essex', reset_cache=False):
|
||||||
@ -489,150 +516,77 @@ def os_release(package, base='essex', reset_cache=False):
|
|||||||
the installation source, the earliest release supported by the charm should
|
the installation source, the earliest release supported by the charm should
|
||||||
be returned.
|
be returned.
|
||||||
'''
|
'''
|
||||||
global os_rel
|
global _os_rel
|
||||||
if reset_cache:
|
if reset_cache:
|
||||||
reset_os_release()
|
reset_os_release()
|
||||||
if os_rel:
|
if _os_rel:
|
||||||
return os_rel
|
return _os_rel
|
||||||
os_rel = (git_os_codename_install_source(config('openstack-origin-git')) or
|
_os_rel = (
|
||||||
get_os_codename_package(package, fatal=False) or
|
git_os_codename_install_source(config('openstack-origin-git')) or
|
||||||
get_os_codename_install_source(config('openstack-origin')) or
|
get_os_codename_package(package, fatal=False) or
|
||||||
base)
|
get_os_codename_install_source(config('openstack-origin')) or
|
||||||
return os_rel
|
base)
|
||||||
|
return _os_rel
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate("moved to charmhelpers.fetch.import_key()", "2017-07", log=juju_log)
|
||||||
def import_key(keyid):
|
def import_key(keyid):
|
||||||
key = keyid.strip()
|
"""Import a key, either ASCII armored, or a GPG key id.
|
||||||
if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
|
|
||||||
key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
|
|
||||||
juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
|
|
||||||
juju_log("Importing ASCII Armor PGP key", level=DEBUG)
|
|
||||||
with tempfile.NamedTemporaryFile() as keyfile:
|
|
||||||
with open(keyfile.name, 'w') as fd:
|
|
||||||
fd.write(key)
|
|
||||||
fd.write("\n")
|
|
||||||
|
|
||||||
cmd = ['apt-key', 'add', keyfile.name]
|
@param keyid: the key in ASCII armor format, or a GPG key id.
|
||||||
try:
|
@raises SystemExit() via sys.exit() on failure.
|
||||||
subprocess.check_call(cmd)
|
"""
|
||||||
except subprocess.CalledProcessError:
|
try:
|
||||||
error_out("Error importing PGP key '%s'" % key)
|
return fetch_import_key(keyid)
|
||||||
else:
|
except GPGKeyError as e:
|
||||||
juju_log("PGP key found (looks like Radix64 format)", level=DEBUG)
|
error_out("Could not import key: {}".format(str(e)))
|
||||||
juju_log("Importing PGP key from keyserver", level=DEBUG)
|
|
||||||
cmd = ['apt-key', 'adv', '--keyserver',
|
|
||||||
'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
|
|
||||||
try:
|
|
||||||
subprocess.check_call(cmd)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
error_out("Error importing PGP key '%s'" % key)
|
|
||||||
|
|
||||||
|
|
||||||
def get_source_and_pgp_key(input):
|
def get_source_and_pgp_key(source_and_key):
|
||||||
"""Look for a pgp key ID or ascii-armor key in the given input."""
|
"""Look for a pgp key ID or ascii-armor key in the given input.
|
||||||
index = input.strip()
|
|
||||||
index = input.rfind('|')
|
|
||||||
if index < 0:
|
|
||||||
return input, None
|
|
||||||
|
|
||||||
key = input[index + 1:].strip('|')
|
:param source_and_key: Sting, "source_spec|keyid" where '|keyid' is
|
||||||
source = input[:index]
|
optional.
|
||||||
return source, key
|
:returns (source_spec, key_id OR None) as a tuple. Returns None for key_id
|
||||||
|
if there was no '|' in the source_and_key string.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source, key = source_and_key.split('|', 2)
|
||||||
|
return source, key or None
|
||||||
|
except ValueError:
|
||||||
|
return source_and_key, None
|
||||||
|
|
||||||
|
|
||||||
def configure_installation_source(rel):
|
@deprecate("use charmhelpers.fetch.add_source() instead.",
|
||||||
'''Configure apt installation source.'''
|
"2017-07", log=juju_log)
|
||||||
if rel == 'distro':
|
def configure_installation_source(source_plus_key):
|
||||||
return
|
"""Configure an installation source.
|
||||||
elif rel == 'distro-proposed':
|
|
||||||
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
|
||||||
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
|
||||||
f.write(DISTRO_PROPOSED % ubuntu_rel)
|
|
||||||
elif rel[:4] == "ppa:":
|
|
||||||
src, key = get_source_and_pgp_key(rel)
|
|
||||||
if key:
|
|
||||||
import_key(key)
|
|
||||||
|
|
||||||
subprocess.check_call(["add-apt-repository", "-y", src])
|
The functionality is provided by charmhelpers.fetch.add_source()
|
||||||
elif rel[:3] == "deb":
|
The difference between the two functions is that add_source() signature
|
||||||
src, key = get_source_and_pgp_key(rel)
|
requires the key to be passed directly, whereas this function passes an
|
||||||
if key:
|
optional key by appending '|<key>' to the end of the source specificiation
|
||||||
import_key(key)
|
'source'.
|
||||||
|
|
||||||
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
Another difference from add_source() is that the function calls sys.exit(1)
|
||||||
f.write(src)
|
if the configuration fails, whereas add_source() raises
|
||||||
elif rel[:6] == 'cloud:':
|
SourceConfigurationError(). Another difference, is that add_source()
|
||||||
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
silently fails (with a juju_log command) if there is no matching source to
|
||||||
rel = rel.split(':')[1]
|
configure, whereas this function fails with a sys.exit(1)
|
||||||
u_rel = rel.split('-')[0]
|
|
||||||
ca_rel = rel.split('-')[1]
|
|
||||||
|
|
||||||
if u_rel != ubuntu_rel:
|
:param source: String_plus_key -- see above for details.
|
||||||
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
|
|
||||||
'version (%s)' % (ca_rel, ubuntu_rel)
|
|
||||||
error_out(e)
|
|
||||||
|
|
||||||
if 'staging' in ca_rel:
|
Note that the behaviour on error is to log the error to the juju log and
|
||||||
# staging is just a regular PPA.
|
then call sys.exit(1).
|
||||||
os_rel = ca_rel.split('/')[0]
|
"""
|
||||||
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
|
# extract the key if there is one, denoted by a '|' in the rel
|
||||||
cmd = 'add-apt-repository -y %s' % ppa
|
source, key = get_source_and_pgp_key(source_plus_key)
|
||||||
subprocess.check_call(cmd.split(' '))
|
|
||||||
return
|
|
||||||
|
|
||||||
# map charm config options to actual archive pockets.
|
# handle the ordinary sources via add_source
|
||||||
pockets = {
|
try:
|
||||||
'folsom': 'precise-updates/folsom',
|
fetch_add_source(source, key, fail_invalid=True)
|
||||||
'folsom/updates': 'precise-updates/folsom',
|
except SourceConfigError as se:
|
||||||
'folsom/proposed': 'precise-proposed/folsom',
|
error_out(str(se))
|
||||||
'grizzly': 'precise-updates/grizzly',
|
|
||||||
'grizzly/updates': 'precise-updates/grizzly',
|
|
||||||
'grizzly/proposed': 'precise-proposed/grizzly',
|
|
||||||
'havana': 'precise-updates/havana',
|
|
||||||
'havana/updates': 'precise-updates/havana',
|
|
||||||
'havana/proposed': 'precise-proposed/havana',
|
|
||||||
'icehouse': 'precise-updates/icehouse',
|
|
||||||
'icehouse/updates': 'precise-updates/icehouse',
|
|
||||||
'icehouse/proposed': 'precise-proposed/icehouse',
|
|
||||||
'juno': 'trusty-updates/juno',
|
|
||||||
'juno/updates': 'trusty-updates/juno',
|
|
||||||
'juno/proposed': 'trusty-proposed/juno',
|
|
||||||
'kilo': 'trusty-updates/kilo',
|
|
||||||
'kilo/updates': 'trusty-updates/kilo',
|
|
||||||
'kilo/proposed': 'trusty-proposed/kilo',
|
|
||||||
'liberty': 'trusty-updates/liberty',
|
|
||||||
'liberty/updates': 'trusty-updates/liberty',
|
|
||||||
'liberty/proposed': 'trusty-proposed/liberty',
|
|
||||||
'mitaka': 'trusty-updates/mitaka',
|
|
||||||
'mitaka/updates': 'trusty-updates/mitaka',
|
|
||||||
'mitaka/proposed': 'trusty-proposed/mitaka',
|
|
||||||
'newton': 'xenial-updates/newton',
|
|
||||||
'newton/updates': 'xenial-updates/newton',
|
|
||||||
'newton/proposed': 'xenial-proposed/newton',
|
|
||||||
'ocata': 'xenial-updates/ocata',
|
|
||||||
'ocata/updates': 'xenial-updates/ocata',
|
|
||||||
'ocata/proposed': 'xenial-proposed/ocata',
|
|
||||||
'pike': 'xenial-updates/pike',
|
|
||||||
'pike/updates': 'xenial-updates/pike',
|
|
||||||
'pike/proposed': 'xenial-proposed/pike',
|
|
||||||
'queens': 'xenial-updates/queens',
|
|
||||||
'queens/updates': 'xenial-updates/queens',
|
|
||||||
'queens/proposed': 'xenial-proposed/queens',
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
pocket = pockets[ca_rel]
|
|
||||||
except KeyError:
|
|
||||||
e = 'Invalid Cloud Archive release specified: %s' % rel
|
|
||||||
error_out(e)
|
|
||||||
|
|
||||||
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
|
|
||||||
apt_install('ubuntu-cloud-keyring', fatal=True)
|
|
||||||
|
|
||||||
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
|
|
||||||
f.write(src)
|
|
||||||
else:
|
|
||||||
error_out("Invalid openstack-release specified: %s" % rel)
|
|
||||||
|
|
||||||
|
|
||||||
def config_value_changed(option):
|
def config_value_changed(option):
|
||||||
@ -677,12 +631,14 @@ def openstack_upgrade_available(package):
|
|||||||
|
|
||||||
:returns: bool: : Returns True if configured installation source offers
|
:returns: bool: : Returns True if configured installation source offers
|
||||||
a newer version of package.
|
a newer version of package.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import apt_pkg as apt
|
import apt_pkg as apt
|
||||||
src = config('openstack-origin')
|
src = config('openstack-origin')
|
||||||
cur_vers = get_os_version_package(package)
|
cur_vers = get_os_version_package(package)
|
||||||
|
if not cur_vers:
|
||||||
|
# The package has not been installed yet do not attempt upgrade
|
||||||
|
return False
|
||||||
if "swift" in package:
|
if "swift" in package:
|
||||||
codename = get_os_codename_install_source(src)
|
codename = get_os_codename_install_source(src)
|
||||||
avail_vers = get_os_version_codename_swift(codename)
|
avail_vers = get_os_version_codename_swift(codename)
|
||||||
@ -1933,6 +1889,30 @@ def pausable_restart_on_change(restart_map, stopstart=False,
|
|||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def ordered(orderme):
|
||||||
|
"""Converts the provided dictionary into a collections.OrderedDict.
|
||||||
|
|
||||||
|
The items in the returned OrderedDict will be inserted based on the
|
||||||
|
natural sort order of the keys. Nested dictionaries will also be sorted
|
||||||
|
in order to ensure fully predictable ordering.
|
||||||
|
|
||||||
|
:param orderme: the dict to order
|
||||||
|
:return: collections.OrderedDict
|
||||||
|
:raises: ValueError: if `orderme` isn't a dict instance.
|
||||||
|
"""
|
||||||
|
if not isinstance(orderme, dict):
|
||||||
|
raise ValueError('argument must be a dict type')
|
||||||
|
|
||||||
|
result = OrderedDict()
|
||||||
|
for k, v in sorted(six.iteritems(orderme), key=lambda x: x[0]):
|
||||||
|
if isinstance(v, dict):
|
||||||
|
result[k] = ordered(v)
|
||||||
|
else:
|
||||||
|
result[k] = v
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def config_flags_parser(config_flags):
|
def config_flags_parser(config_flags):
|
||||||
"""Parses config flags string into dict.
|
"""Parses config flags string into dict.
|
||||||
|
|
||||||
@ -1944,15 +1924,13 @@ def config_flags_parser(config_flags):
|
|||||||
example, a string in the format of 'key1=value1, key2=value2' will
|
example, a string in the format of 'key1=value1, key2=value2' will
|
||||||
return a dict of:
|
return a dict of:
|
||||||
|
|
||||||
{'key1': 'value1',
|
{'key1': 'value1', 'key2': 'value2'}.
|
||||||
'key2': 'value2'}.
|
|
||||||
|
|
||||||
2. A string in the above format, but supporting a comma-delimited list
|
2. A string in the above format, but supporting a comma-delimited list
|
||||||
of values for the same key. For example, a string in the format of
|
of values for the same key. For example, a string in the format of
|
||||||
'key1=value1, key2=value3,value4,value5' will return a dict of:
|
'key1=value1, key2=value3,value4,value5' will return a dict of:
|
||||||
|
|
||||||
{'key1', 'value1',
|
{'key1': 'value1', 'key2': 'value2,value3,value4'}
|
||||||
'key2', 'value2,value3,value4'}
|
|
||||||
|
|
||||||
3. A string containing a colon character (:) prior to an equal
|
3. A string containing a colon character (:) prior to an equal
|
||||||
character (=) will be treated as yaml and parsed as such. This can be
|
character (=) will be treated as yaml and parsed as such. This can be
|
||||||
@ -1972,7 +1950,7 @@ def config_flags_parser(config_flags):
|
|||||||
equals = config_flags.find('=')
|
equals = config_flags.find('=')
|
||||||
if colon > 0:
|
if colon > 0:
|
||||||
if colon < equals or equals < 0:
|
if colon < equals or equals < 0:
|
||||||
return yaml.safe_load(config_flags)
|
return ordered(yaml.safe_load(config_flags))
|
||||||
|
|
||||||
if config_flags.find('==') >= 0:
|
if config_flags.find('==') >= 0:
|
||||||
juju_log("config_flags is not in expected format (key=value)",
|
juju_log("config_flags is not in expected format (key=value)",
|
||||||
@ -1985,7 +1963,7 @@ def config_flags_parser(config_flags):
|
|||||||
# split on '='.
|
# split on '='.
|
||||||
split = config_flags.strip(' =').split('=')
|
split = config_flags.strip(' =').split('=')
|
||||||
limit = len(split)
|
limit = len(split)
|
||||||
flags = {}
|
flags = OrderedDict()
|
||||||
for i in range(0, limit - 1):
|
for i in range(0, limit - 1):
|
||||||
current = split[i]
|
current = split[i]
|
||||||
next = split[i + 1]
|
next = split[i + 1]
|
||||||
@ -2052,3 +2030,84 @@ def token_cache_pkgs(source=None, release=None):
|
|||||||
if enable_memcache(source=source, release=release):
|
if enable_memcache(source=source, release=release):
|
||||||
packages.extend(['memcached', 'python-memcache'])
|
packages.extend(['memcached', 'python-memcache'])
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def update_json_file(filename, items):
|
||||||
|
"""Updates the json `filename` with a given dict.
|
||||||
|
:param filename: json filename (i.e.: /etc/glance/policy.json)
|
||||||
|
:param items: dict of items to update
|
||||||
|
"""
|
||||||
|
with open(filename) as fd:
|
||||||
|
policy = json.load(fd)
|
||||||
|
policy.update(items)
|
||||||
|
with open(filename, "w") as fd:
|
||||||
|
fd.write(json.dumps(policy, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def snap_install_requested():
|
||||||
|
""" Determine if installing from snaps
|
||||||
|
|
||||||
|
If openstack-origin is of the form snap:channel-series-release
|
||||||
|
and channel is in SNAPS_CHANNELS return True.
|
||||||
|
"""
|
||||||
|
origin = config('openstack-origin') or ""
|
||||||
|
if not origin.startswith('snap:'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
_src = origin[5:]
|
||||||
|
channel, series, release = _src.split('-')
|
||||||
|
if channel.lower() in SNAP_CHANNELS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_snaps_install_info_from_origin(snaps, src, mode='classic'):
|
||||||
|
"""Generate a dictionary of snap install information from origin
|
||||||
|
|
||||||
|
@param snaps: List of snaps
|
||||||
|
@param src: String of openstack-origin or source of the form
|
||||||
|
snap:channel-series-track
|
||||||
|
@param mode: String classic, devmode or jailmode
|
||||||
|
@returns: Dictionary of snaps with channels and modes
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not src.startswith('snap:'):
|
||||||
|
juju_log("Snap source is not a snap origin", 'WARN')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
_src = src[5:]
|
||||||
|
_channel, _series, _release = _src.split('-')
|
||||||
|
channel = '--channel={}/{}'.format(_release, _channel)
|
||||||
|
|
||||||
|
return {snap: {'channel': channel, 'mode': mode}
|
||||||
|
for snap in snaps}
|
||||||
|
|
||||||
|
|
||||||
|
def install_os_snaps(snaps, refresh=False):
|
||||||
|
"""Install OpenStack snaps from channel and with mode
|
||||||
|
|
||||||
|
@param snaps: Dictionary of snaps with channels and modes of the form:
|
||||||
|
{'snap_name': {'channel': 'snap_channel',
|
||||||
|
'mode': 'snap_mode'}}
|
||||||
|
Where channel a snapstore channel and mode is --classic, --devmode or
|
||||||
|
--jailmode.
|
||||||
|
@param post_snap_install: Callback function to run after snaps have been
|
||||||
|
installed
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _ensure_flag(flag):
|
||||||
|
if flag.startswith('--'):
|
||||||
|
return flag
|
||||||
|
return '--{}'.format(flag)
|
||||||
|
|
||||||
|
if refresh:
|
||||||
|
for snap in snaps.keys():
|
||||||
|
snap_refresh(snap,
|
||||||
|
_ensure_flag(snaps[snap]['channel']),
|
||||||
|
_ensure_flag(snaps[snap]['mode']))
|
||||||
|
else:
|
||||||
|
for snap in snaps.keys():
|
||||||
|
snap_install(snap,
|
||||||
|
_ensure_flag(snaps[snap]['channel']),
|
||||||
|
_ensure_flag(snaps[snap]['mode']))
|
||||||
|
74
charmhelpers/contrib/storage/linux/bcache.py
Normal file
74
charmhelpers/contrib/storage/linux/bcache.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Copyright 2017 Canonical Limited.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import log
|
||||||
|
|
||||||
|
stats_intervals = ['stats_day', 'stats_five_minute',
|
||||||
|
'stats_hour', 'stats_total']
|
||||||
|
|
||||||
|
SYSFS = '/sys'
|
||||||
|
|
||||||
|
|
||||||
|
class Bcache(object):
|
||||||
|
"""Bcache behaviour
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cachepath):
|
||||||
|
self.cachepath = cachepath
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromdevice(cls, devname):
|
||||||
|
return cls('{}/block/{}/bcache'.format(SYSFS, devname))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.cachepath
|
||||||
|
|
||||||
|
def get_stats(self, interval):
|
||||||
|
"""Get cache stats
|
||||||
|
"""
|
||||||
|
intervaldir = 'stats_{}'.format(interval)
|
||||||
|
path = "{}/{}".format(self.cachepath, intervaldir)
|
||||||
|
out = dict()
|
||||||
|
for elem in os.listdir(path):
|
||||||
|
out[elem] = open('{}/{}'.format(path, elem)).read().strip()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_bcache_fs():
|
||||||
|
"""Return all cache sets
|
||||||
|
"""
|
||||||
|
cachesetroot = "{}/fs/bcache".format(SYSFS)
|
||||||
|
try:
|
||||||
|
dirs = os.listdir(cachesetroot)
|
||||||
|
except OSError:
|
||||||
|
log("No bcache fs found")
|
||||||
|
return []
|
||||||
|
cacheset = set([Bcache('{}/{}'.format(cachesetroot, d)) for d in dirs if not d.startswith('register')])
|
||||||
|
return cacheset
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats_action(cachespec, interval):
|
||||||
|
"""Action for getting bcache statistics for a given cachespec.
|
||||||
|
Cachespec can either be a device name, eg. 'sdb', which will retrieve
|
||||||
|
cache stats for the given device, or 'global', which will retrieve stats
|
||||||
|
for all cachesets
|
||||||
|
"""
|
||||||
|
if cachespec == 'global':
|
||||||
|
caches = get_bcache_fs()
|
||||||
|
else:
|
||||||
|
caches = [Bcache.fromdevice(cachespec)]
|
||||||
|
res = dict((c.cachepath, c.get_stats(interval)) for c in caches)
|
||||||
|
return json.dumps(res, indent=4, separators=(',', ': '))
|
@ -63,6 +63,7 @@ from charmhelpers.core.host import (
|
|||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
apt_install,
|
apt_install,
|
||||||
)
|
)
|
||||||
|
from charmhelpers.core.unitdata import kv
|
||||||
|
|
||||||
from charmhelpers.core.kernel import modprobe
|
from charmhelpers.core.kernel import modprobe
|
||||||
from charmhelpers.contrib.openstack.utils import config_flags_parser
|
from charmhelpers.contrib.openstack.utils import config_flags_parser
|
||||||
@ -1314,6 +1315,47 @@ def send_request_if_needed(request, relation='ceph'):
|
|||||||
relation_set(relation_id=rid, broker_req=request.request)
|
relation_set(relation_id=rid, broker_req=request.request)
|
||||||
|
|
||||||
|
|
||||||
|
def is_broker_action_done(action, rid=None, unit=None):
|
||||||
|
"""Check whether broker action has completed yet.
|
||||||
|
|
||||||
|
@param action: name of action to be performed
|
||||||
|
@returns True if action complete otherwise False
|
||||||
|
"""
|
||||||
|
rdata = relation_get(rid, unit) or {}
|
||||||
|
broker_rsp = rdata.get(get_broker_rsp_key())
|
||||||
|
if not broker_rsp:
|
||||||
|
return False
|
||||||
|
|
||||||
|
rsp = CephBrokerRsp(broker_rsp)
|
||||||
|
unit_name = local_unit().partition('/')[2]
|
||||||
|
key = "unit_{}_ceph_broker_action.{}".format(unit_name, action)
|
||||||
|
kvstore = kv()
|
||||||
|
val = kvstore.get(key=key)
|
||||||
|
if val and val == rsp.request_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def mark_broker_action_done(action, rid=None, unit=None):
|
||||||
|
"""Mark action as having been completed.
|
||||||
|
|
||||||
|
@param action: name of action to be performed
|
||||||
|
@returns None
|
||||||
|
"""
|
||||||
|
rdata = relation_get(rid, unit) or {}
|
||||||
|
broker_rsp = rdata.get(get_broker_rsp_key())
|
||||||
|
if not broker_rsp:
|
||||||
|
return
|
||||||
|
|
||||||
|
rsp = CephBrokerRsp(broker_rsp)
|
||||||
|
unit_name = local_unit().partition('/')[2]
|
||||||
|
key = "unit_{}_ceph_broker_action.{}".format(unit_name, action)
|
||||||
|
kvstore = kv()
|
||||||
|
kvstore.set(key=key, value=rsp.request_id)
|
||||||
|
kvstore.flush()
|
||||||
|
|
||||||
|
|
||||||
class CephConfContext(object):
|
class CephConfContext(object):
|
||||||
"""Ceph config (ceph.conf) context.
|
"""Ceph config (ceph.conf) context.
|
||||||
|
|
||||||
@ -1330,7 +1372,7 @@ class CephConfContext(object):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
conf = config_flags_parser(conf)
|
conf = config_flags_parser(conf)
|
||||||
if type(conf) != dict:
|
if not isinstance(conf, dict):
|
||||||
log("Provided config-flags is not a dictionary - ignoring",
|
log("Provided config-flags is not a dictionary - ignoring",
|
||||||
level=WARNING)
|
level=WARNING)
|
||||||
return {}
|
return {}
|
||||||
|
@ -202,6 +202,27 @@ def service_name():
|
|||||||
return local_unit().split('/')[0]
|
return local_unit().split('/')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def principal_unit():
|
||||||
|
"""Returns the principal unit of this unit, otherwise None"""
|
||||||
|
# Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT
|
||||||
|
principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None)
|
||||||
|
# If it's empty, then this unit is the principal
|
||||||
|
if principal_unit == '':
|
||||||
|
return os.environ['JUJU_UNIT_NAME']
|
||||||
|
elif principal_unit is not None:
|
||||||
|
return principal_unit
|
||||||
|
# For Juju 2.1 and below, let's try work out the principle unit by
|
||||||
|
# the various charms' metadata.yaml.
|
||||||
|
for reltype in relation_types():
|
||||||
|
for rid in relation_ids(reltype):
|
||||||
|
for unit in related_units(rid):
|
||||||
|
md = _metadata_unit(unit)
|
||||||
|
subordinate = md.pop('subordinate', None)
|
||||||
|
if not subordinate:
|
||||||
|
return unit
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def remote_service_name(relid=None):
|
def remote_service_name(relid=None):
|
||||||
"""The remote service name for a given relation-id (or the current relation)"""
|
"""The remote service name for a given relation-id (or the current relation)"""
|
||||||
@ -478,6 +499,21 @@ def metadata():
|
|||||||
return yaml.safe_load(md)
|
return yaml.safe_load(md)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_unit(unit):
|
||||||
|
"""Given the name of a unit (e.g. apache2/0), get the unit charm's
|
||||||
|
metadata.yaml. Very similar to metadata() but allows us to inspect
|
||||||
|
other units. Unit needs to be co-located, such as a subordinate or
|
||||||
|
principal/primary.
|
||||||
|
|
||||||
|
:returns: metadata.yaml as a python object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
basedir = os.sep.join(charm_dir().split(os.sep)[:-2])
|
||||||
|
unitdir = 'unit-{}'.format(unit.replace(os.sep, '-'))
|
||||||
|
with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md:
|
||||||
|
return yaml.safe_load(md)
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def relation_types():
|
def relation_types():
|
||||||
"""Get a list of relation types supported by this charm"""
|
"""Get a list of relation types supported by this charm"""
|
||||||
@ -753,6 +789,9 @@ class Hooks(object):
|
|||||||
|
|
||||||
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')
|
||||||
|
if d is not None:
|
||||||
|
return d
|
||||||
return os.environ.get('CHARM_DIR')
|
return os.environ.get('CHARM_DIR')
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +191,7 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
|
|||||||
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
|
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
|
||||||
sysv_file = os.path.join(initd_dir, service_name)
|
sysv_file = os.path.join(initd_dir, service_name)
|
||||||
if init_is_systemd():
|
if init_is_systemd():
|
||||||
|
service('disable', service_name)
|
||||||
service('mask', service_name)
|
service('mask', service_name)
|
||||||
elif os.path.exists(upstart_file):
|
elif os.path.exists(upstart_file):
|
||||||
override_path = os.path.join(
|
override_path = os.path.join(
|
||||||
@ -225,6 +226,7 @@ def service_resume(service_name, init_dir="/etc/init",
|
|||||||
sysv_file = os.path.join(initd_dir, service_name)
|
sysv_file = os.path.join(initd_dir, service_name)
|
||||||
if init_is_systemd():
|
if init_is_systemd():
|
||||||
service('unmask', service_name)
|
service('unmask', service_name)
|
||||||
|
service('enable', service_name)
|
||||||
elif os.path.exists(upstart_file):
|
elif os.path.exists(upstart_file):
|
||||||
override_path = os.path.join(
|
override_path = os.path.join(
|
||||||
init_dir, '{}.override'.format(service_name))
|
init_dir, '{}.override'.format(service_name))
|
||||||
|
@ -48,6 +48,13 @@ class AptLockError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GPGKeyError(Exception):
|
||||||
|
"""Exception occurs when a GPG key cannot be fetched or used. The message
|
||||||
|
indicates what the problem is.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseFetchHandler(object):
|
class BaseFetchHandler(object):
|
||||||
|
|
||||||
"""Base class for FetchHandler implementations in fetch plugins"""
|
"""Base class for FetchHandler implementations in fetch plugins"""
|
||||||
@ -77,21 +84,22 @@ module = "charmhelpers.fetch.%s" % __platform__
|
|||||||
fetch = importlib.import_module(module)
|
fetch = importlib.import_module(module)
|
||||||
|
|
||||||
filter_installed_packages = fetch.filter_installed_packages
|
filter_installed_packages = fetch.filter_installed_packages
|
||||||
install = fetch.install
|
install = fetch.apt_install
|
||||||
upgrade = fetch.upgrade
|
upgrade = fetch.apt_upgrade
|
||||||
update = fetch.update
|
update = _fetch_update = fetch.apt_update
|
||||||
purge = fetch.purge
|
purge = fetch.apt_purge
|
||||||
add_source = fetch.add_source
|
add_source = fetch.add_source
|
||||||
|
|
||||||
if __platform__ == "ubuntu":
|
if __platform__ == "ubuntu":
|
||||||
apt_cache = fetch.apt_cache
|
apt_cache = fetch.apt_cache
|
||||||
apt_install = fetch.install
|
apt_install = fetch.apt_install
|
||||||
apt_update = fetch.update
|
apt_update = fetch.apt_update
|
||||||
apt_upgrade = fetch.upgrade
|
apt_upgrade = fetch.apt_upgrade
|
||||||
apt_purge = fetch.purge
|
apt_purge = fetch.apt_purge
|
||||||
apt_mark = fetch.apt_mark
|
apt_mark = fetch.apt_mark
|
||||||
apt_hold = fetch.apt_hold
|
apt_hold = fetch.apt_hold
|
||||||
apt_unhold = fetch.apt_unhold
|
apt_unhold = fetch.apt_unhold
|
||||||
|
import_key = fetch.import_key
|
||||||
get_upstream_version = fetch.get_upstream_version
|
get_upstream_version = fetch.get_upstream_version
|
||||||
elif __platform__ == "centos":
|
elif __platform__ == "centos":
|
||||||
yum_search = fetch.yum_search
|
yum_search = fetch.yum_search
|
||||||
@ -135,7 +143,7 @@ def configure_sources(update=False,
|
|||||||
for source, key in zip(sources, keys):
|
for source, key in zip(sources, keys):
|
||||||
add_source(source, key)
|
add_source(source, key)
|
||||||
if update:
|
if update:
|
||||||
fetch.update(fatal=True)
|
_fetch_update(fatal=True)
|
||||||
|
|
||||||
|
|
||||||
def install_remote(source, *args, **kwargs):
|
def install_remote(source, *args, **kwargs):
|
||||||
|
@ -132,7 +132,7 @@ def add_source(source, key=None):
|
|||||||
key_file.write(key)
|
key_file.write(key)
|
||||||
key_file.flush()
|
key_file.flush()
|
||||||
key_file.seek(0)
|
key_file.seek(0)
|
||||||
subprocess.check_call(['rpm', '--import', key_file])
|
subprocess.check_call(['rpm', '--import', key_file.name])
|
||||||
else:
|
else:
|
||||||
subprocess.check_call(['rpm', '--import', key])
|
subprocess.check_call(['rpm', '--import', key])
|
||||||
|
|
||||||
|
@ -18,15 +18,23 @@ If writing reactive charms, use the snap layer:
|
|||||||
https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
|
https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
|
||||||
"""
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
from os import environ
|
import os
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from charmhelpers.core.hookenv import log
|
from charmhelpers.core.hookenv import log
|
||||||
|
|
||||||
__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
|
__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
|
||||||
|
|
||||||
SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
|
# The return code for "couldn't acquire lock" in Snap
|
||||||
|
# (hopefully this will be improved).
|
||||||
|
SNAP_NO_LOCK = 1
|
||||||
SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
|
SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
|
||||||
SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
|
SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
|
||||||
|
SNAP_CHANNELS = [
|
||||||
|
'edge',
|
||||||
|
'beta',
|
||||||
|
'candidate',
|
||||||
|
'stable',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CouldNotAcquireLockException(Exception):
|
class CouldNotAcquireLockException(Exception):
|
||||||
@ -47,13 +55,17 @@ def _snap_exec(commands):
|
|||||||
|
|
||||||
while return_code is None or return_code == SNAP_NO_LOCK:
|
while return_code is None or return_code == SNAP_NO_LOCK:
|
||||||
try:
|
try:
|
||||||
return_code = subprocess.check_call(['snap'] + commands, env=environ)
|
return_code = subprocess.check_call(['snap'] + commands,
|
||||||
|
env=os.environ)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
retry_count += + 1
|
retry_count += + 1
|
||||||
if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
|
if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
|
||||||
raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
|
raise CouldNotAcquireLockException(
|
||||||
|
'Could not aquire lock after {} attempts'
|
||||||
|
.format(SNAP_NO_LOCK_RETRY_COUNT))
|
||||||
return_code = e.returncode
|
return_code = e.returncode
|
||||||
log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
|
log('Snap failed to acquire lock, trying again in {} seconds.'
|
||||||
|
.format(SNAP_NO_LOCK_RETRY_DELAY, level='WARN'))
|
||||||
sleep(SNAP_NO_LOCK_RETRY_DELAY)
|
sleep(SNAP_NO_LOCK_RETRY_DELAY)
|
||||||
|
|
||||||
return return_code
|
return return_code
|
||||||
|
@ -12,29 +12,47 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
import six
|
import six
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
from charmhelpers.core.host import (
|
from charmhelpers.core.host import (
|
||||||
lsb_release
|
lsb_release
|
||||||
)
|
)
|
||||||
from charmhelpers.core.hookenv import log
|
from charmhelpers.core.hookenv import (
|
||||||
from charmhelpers.fetch import SourceConfigError
|
log,
|
||||||
|
DEBUG,
|
||||||
|
)
|
||||||
|
from charmhelpers.fetch import SourceConfigError, GPGKeyError
|
||||||
|
|
||||||
|
PROPOSED_POCKET = (
|
||||||
|
"# Proposed\n"
|
||||||
|
"deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
|
||||||
|
"multiverse restricted\n")
|
||||||
|
PROPOSED_PORTS_POCKET = (
|
||||||
|
"# Proposed\n"
|
||||||
|
"deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
|
||||||
|
"multiverse restricted\n")
|
||||||
|
# Only supports 64bit and ppc64 at the moment.
|
||||||
|
ARCH_TO_PROPOSED_POCKET = {
|
||||||
|
'x86_64': PROPOSED_POCKET,
|
||||||
|
'ppc64le': PROPOSED_PORTS_POCKET,
|
||||||
|
'aarch64': PROPOSED_PORTS_POCKET,
|
||||||
|
}
|
||||||
|
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
||||||
|
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
||||||
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
|
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
|
||||||
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
|
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PROPOSED_POCKET = """# Proposed
|
|
||||||
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
|
|
||||||
"""
|
|
||||||
|
|
||||||
CLOUD_ARCHIVE_POCKETS = {
|
CLOUD_ARCHIVE_POCKETS = {
|
||||||
# Folsom
|
# Folsom
|
||||||
'folsom': 'precise-updates/folsom',
|
'folsom': 'precise-updates/folsom',
|
||||||
|
'folsom/updates': 'precise-updates/folsom',
|
||||||
'precise-folsom': 'precise-updates/folsom',
|
'precise-folsom': 'precise-updates/folsom',
|
||||||
'precise-folsom/updates': 'precise-updates/folsom',
|
'precise-folsom/updates': 'precise-updates/folsom',
|
||||||
'precise-updates/folsom': 'precise-updates/folsom',
|
'precise-updates/folsom': 'precise-updates/folsom',
|
||||||
@ -43,6 +61,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'precise-proposed/folsom': 'precise-proposed/folsom',
|
'precise-proposed/folsom': 'precise-proposed/folsom',
|
||||||
# Grizzly
|
# Grizzly
|
||||||
'grizzly': 'precise-updates/grizzly',
|
'grizzly': 'precise-updates/grizzly',
|
||||||
|
'grizzly/updates': 'precise-updates/grizzly',
|
||||||
'precise-grizzly': 'precise-updates/grizzly',
|
'precise-grizzly': 'precise-updates/grizzly',
|
||||||
'precise-grizzly/updates': 'precise-updates/grizzly',
|
'precise-grizzly/updates': 'precise-updates/grizzly',
|
||||||
'precise-updates/grizzly': 'precise-updates/grizzly',
|
'precise-updates/grizzly': 'precise-updates/grizzly',
|
||||||
@ -51,6 +70,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'precise-proposed/grizzly': 'precise-proposed/grizzly',
|
'precise-proposed/grizzly': 'precise-proposed/grizzly',
|
||||||
# Havana
|
# Havana
|
||||||
'havana': 'precise-updates/havana',
|
'havana': 'precise-updates/havana',
|
||||||
|
'havana/updates': 'precise-updates/havana',
|
||||||
'precise-havana': 'precise-updates/havana',
|
'precise-havana': 'precise-updates/havana',
|
||||||
'precise-havana/updates': 'precise-updates/havana',
|
'precise-havana/updates': 'precise-updates/havana',
|
||||||
'precise-updates/havana': 'precise-updates/havana',
|
'precise-updates/havana': 'precise-updates/havana',
|
||||||
@ -59,6 +79,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'precise-proposed/havana': 'precise-proposed/havana',
|
'precise-proposed/havana': 'precise-proposed/havana',
|
||||||
# Icehouse
|
# Icehouse
|
||||||
'icehouse': 'precise-updates/icehouse',
|
'icehouse': 'precise-updates/icehouse',
|
||||||
|
'icehouse/updates': 'precise-updates/icehouse',
|
||||||
'precise-icehouse': 'precise-updates/icehouse',
|
'precise-icehouse': 'precise-updates/icehouse',
|
||||||
'precise-icehouse/updates': 'precise-updates/icehouse',
|
'precise-icehouse/updates': 'precise-updates/icehouse',
|
||||||
'precise-updates/icehouse': 'precise-updates/icehouse',
|
'precise-updates/icehouse': 'precise-updates/icehouse',
|
||||||
@ -67,6 +88,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'precise-proposed/icehouse': 'precise-proposed/icehouse',
|
'precise-proposed/icehouse': 'precise-proposed/icehouse',
|
||||||
# Juno
|
# Juno
|
||||||
'juno': 'trusty-updates/juno',
|
'juno': 'trusty-updates/juno',
|
||||||
|
'juno/updates': 'trusty-updates/juno',
|
||||||
'trusty-juno': 'trusty-updates/juno',
|
'trusty-juno': 'trusty-updates/juno',
|
||||||
'trusty-juno/updates': 'trusty-updates/juno',
|
'trusty-juno/updates': 'trusty-updates/juno',
|
||||||
'trusty-updates/juno': 'trusty-updates/juno',
|
'trusty-updates/juno': 'trusty-updates/juno',
|
||||||
@ -75,6 +97,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'trusty-proposed/juno': 'trusty-proposed/juno',
|
'trusty-proposed/juno': 'trusty-proposed/juno',
|
||||||
# Kilo
|
# Kilo
|
||||||
'kilo': 'trusty-updates/kilo',
|
'kilo': 'trusty-updates/kilo',
|
||||||
|
'kilo/updates': 'trusty-updates/kilo',
|
||||||
'trusty-kilo': 'trusty-updates/kilo',
|
'trusty-kilo': 'trusty-updates/kilo',
|
||||||
'trusty-kilo/updates': 'trusty-updates/kilo',
|
'trusty-kilo/updates': 'trusty-updates/kilo',
|
||||||
'trusty-updates/kilo': 'trusty-updates/kilo',
|
'trusty-updates/kilo': 'trusty-updates/kilo',
|
||||||
@ -83,6 +106,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'trusty-proposed/kilo': 'trusty-proposed/kilo',
|
'trusty-proposed/kilo': 'trusty-proposed/kilo',
|
||||||
# Liberty
|
# Liberty
|
||||||
'liberty': 'trusty-updates/liberty',
|
'liberty': 'trusty-updates/liberty',
|
||||||
|
'liberty/updates': 'trusty-updates/liberty',
|
||||||
'trusty-liberty': 'trusty-updates/liberty',
|
'trusty-liberty': 'trusty-updates/liberty',
|
||||||
'trusty-liberty/updates': 'trusty-updates/liberty',
|
'trusty-liberty/updates': 'trusty-updates/liberty',
|
||||||
'trusty-updates/liberty': 'trusty-updates/liberty',
|
'trusty-updates/liberty': 'trusty-updates/liberty',
|
||||||
@ -91,6 +115,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'trusty-proposed/liberty': 'trusty-proposed/liberty',
|
'trusty-proposed/liberty': 'trusty-proposed/liberty',
|
||||||
# Mitaka
|
# Mitaka
|
||||||
'mitaka': 'trusty-updates/mitaka',
|
'mitaka': 'trusty-updates/mitaka',
|
||||||
|
'mitaka/updates': 'trusty-updates/mitaka',
|
||||||
'trusty-mitaka': 'trusty-updates/mitaka',
|
'trusty-mitaka': 'trusty-updates/mitaka',
|
||||||
'trusty-mitaka/updates': 'trusty-updates/mitaka',
|
'trusty-mitaka/updates': 'trusty-updates/mitaka',
|
||||||
'trusty-updates/mitaka': 'trusty-updates/mitaka',
|
'trusty-updates/mitaka': 'trusty-updates/mitaka',
|
||||||
@ -99,6 +124,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
|
'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
|
||||||
# Newton
|
# Newton
|
||||||
'newton': 'xenial-updates/newton',
|
'newton': 'xenial-updates/newton',
|
||||||
|
'newton/updates': 'xenial-updates/newton',
|
||||||
'xenial-newton': 'xenial-updates/newton',
|
'xenial-newton': 'xenial-updates/newton',
|
||||||
'xenial-newton/updates': 'xenial-updates/newton',
|
'xenial-newton/updates': 'xenial-updates/newton',
|
||||||
'xenial-updates/newton': 'xenial-updates/newton',
|
'xenial-updates/newton': 'xenial-updates/newton',
|
||||||
@ -107,12 +133,13 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'xenial-proposed/newton': 'xenial-proposed/newton',
|
'xenial-proposed/newton': 'xenial-proposed/newton',
|
||||||
# Ocata
|
# Ocata
|
||||||
'ocata': 'xenial-updates/ocata',
|
'ocata': 'xenial-updates/ocata',
|
||||||
|
'ocata/updates': 'xenial-updates/ocata',
|
||||||
'xenial-ocata': 'xenial-updates/ocata',
|
'xenial-ocata': 'xenial-updates/ocata',
|
||||||
'xenial-ocata/updates': 'xenial-updates/ocata',
|
'xenial-ocata/updates': 'xenial-updates/ocata',
|
||||||
'xenial-updates/ocata': 'xenial-updates/ocata',
|
'xenial-updates/ocata': 'xenial-updates/ocata',
|
||||||
'ocata/proposed': 'xenial-proposed/ocata',
|
'ocata/proposed': 'xenial-proposed/ocata',
|
||||||
'xenial-ocata/proposed': 'xenial-proposed/ocata',
|
'xenial-ocata/proposed': 'xenial-proposed/ocata',
|
||||||
'xenial-ocata/newton': 'xenial-proposed/ocata',
|
'xenial-proposed/ocata': 'xenial-proposed/ocata',
|
||||||
# Pike
|
# Pike
|
||||||
'pike': 'xenial-updates/pike',
|
'pike': 'xenial-updates/pike',
|
||||||
'xenial-pike': 'xenial-updates/pike',
|
'xenial-pike': 'xenial-updates/pike',
|
||||||
@ -120,7 +147,7 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'xenial-updates/pike': 'xenial-updates/pike',
|
'xenial-updates/pike': 'xenial-updates/pike',
|
||||||
'pike/proposed': 'xenial-proposed/pike',
|
'pike/proposed': 'xenial-proposed/pike',
|
||||||
'xenial-pike/proposed': 'xenial-proposed/pike',
|
'xenial-pike/proposed': 'xenial-proposed/pike',
|
||||||
'xenial-pike/newton': 'xenial-proposed/pike',
|
'xenial-proposed/pike': 'xenial-proposed/pike',
|
||||||
# Queens
|
# Queens
|
||||||
'queens': 'xenial-updates/queens',
|
'queens': 'xenial-updates/queens',
|
||||||
'xenial-queens': 'xenial-updates/queens',
|
'xenial-queens': 'xenial-updates/queens',
|
||||||
@ -128,12 +155,13 @@ CLOUD_ARCHIVE_POCKETS = {
|
|||||||
'xenial-updates/queens': 'xenial-updates/queens',
|
'xenial-updates/queens': 'xenial-updates/queens',
|
||||||
'queens/proposed': 'xenial-proposed/queens',
|
'queens/proposed': 'xenial-proposed/queens',
|
||||||
'xenial-queens/proposed': 'xenial-proposed/queens',
|
'xenial-queens/proposed': 'xenial-proposed/queens',
|
||||||
'xenial-queens/newton': 'xenial-proposed/queens',
|
'xenial-proposed/queens': 'xenial-proposed/queens',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
|
APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
|
||||||
CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
|
CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
|
||||||
CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
|
CMD_RETRY_COUNT = 3 # Retry a failing fatal command X times.
|
||||||
|
|
||||||
|
|
||||||
def filter_installed_packages(packages):
|
def filter_installed_packages(packages):
|
||||||
@ -161,7 +189,7 @@ def apt_cache(in_memory=True, progress=None):
|
|||||||
return apt_pkg.Cache(progress)
|
return apt_pkg.Cache(progress)
|
||||||
|
|
||||||
|
|
||||||
def install(packages, options=None, fatal=False):
|
def apt_install(packages, options=None, fatal=False):
|
||||||
"""Install one or more packages."""
|
"""Install one or more packages."""
|
||||||
if options is None:
|
if options is None:
|
||||||
options = ['--option=Dpkg::Options::=--force-confold']
|
options = ['--option=Dpkg::Options::=--force-confold']
|
||||||
@ -178,7 +206,7 @@ def install(packages, options=None, fatal=False):
|
|||||||
_run_apt_command(cmd, fatal)
|
_run_apt_command(cmd, fatal)
|
||||||
|
|
||||||
|
|
||||||
def upgrade(options=None, fatal=False, dist=False):
|
def apt_upgrade(options=None, fatal=False, dist=False):
|
||||||
"""Upgrade all packages."""
|
"""Upgrade all packages."""
|
||||||
if options is None:
|
if options is None:
|
||||||
options = ['--option=Dpkg::Options::=--force-confold']
|
options = ['--option=Dpkg::Options::=--force-confold']
|
||||||
@ -193,13 +221,13 @@ def upgrade(options=None, fatal=False, dist=False):
|
|||||||
_run_apt_command(cmd, fatal)
|
_run_apt_command(cmd, fatal)
|
||||||
|
|
||||||
|
|
||||||
def update(fatal=False):
|
def apt_update(fatal=False):
|
||||||
"""Update local apt cache."""
|
"""Update local apt cache."""
|
||||||
cmd = ['apt-get', 'update']
|
cmd = ['apt-get', 'update']
|
||||||
_run_apt_command(cmd, fatal)
|
_run_apt_command(cmd, fatal)
|
||||||
|
|
||||||
|
|
||||||
def purge(packages, fatal=False):
|
def apt_purge(packages, fatal=False):
|
||||||
"""Purge one or more packages."""
|
"""Purge one or more packages."""
|
||||||
cmd = ['apt-get', '--assume-yes', 'purge']
|
cmd = ['apt-get', '--assume-yes', 'purge']
|
||||||
if isinstance(packages, six.string_types):
|
if isinstance(packages, six.string_types):
|
||||||
@ -233,7 +261,45 @@ def apt_unhold(packages, fatal=False):
|
|||||||
return apt_mark(packages, 'unhold', fatal=fatal)
|
return apt_mark(packages, 'unhold', fatal=fatal)
|
||||||
|
|
||||||
|
|
||||||
def add_source(source, key=None):
|
def import_key(keyid):
|
||||||
|
"""Import a key in either ASCII Armor or Radix64 format.
|
||||||
|
|
||||||
|
`keyid` is either the keyid to fetch from a PGP server, or
|
||||||
|
the key in ASCII armor foramt.
|
||||||
|
|
||||||
|
:param keyid: String of key (or key id).
|
||||||
|
:raises: GPGKeyError if the key could not be imported
|
||||||
|
"""
|
||||||
|
key = keyid.strip()
|
||||||
|
if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
|
||||||
|
key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
|
||||||
|
log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
|
||||||
|
log("Importing ASCII Armor PGP key", level=DEBUG)
|
||||||
|
with NamedTemporaryFile() as keyfile:
|
||||||
|
with open(keyfile.name, 'w') as fd:
|
||||||
|
fd.write(key)
|
||||||
|
fd.write("\n")
|
||||||
|
cmd = ['apt-key', 'add', keyfile.name]
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
error = "Error importing PGP key '{}'".format(key)
|
||||||
|
log(error)
|
||||||
|
raise GPGKeyError(error)
|
||||||
|
else:
|
||||||
|
log("PGP key found (looks like Radix64 format)", level=DEBUG)
|
||||||
|
log("Importing PGP key from keyserver", level=DEBUG)
|
||||||
|
cmd = ['apt-key', 'adv', '--keyserver',
|
||||||
|
'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
error = "Error importing PGP key '{}'".format(key)
|
||||||
|
log(error)
|
||||||
|
raise GPGKeyError(error)
|
||||||
|
|
||||||
|
|
||||||
|
def add_source(source, key=None, fail_invalid=False):
|
||||||
"""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
|
||||||
@ -249,6 +315,33 @@ def add_source(source, key=None):
|
|||||||
such as 'cloud:icehouse'
|
such as 'cloud:icehouse'
|
||||||
'distro' may be used as a noop
|
'distro' may be used as a noop
|
||||||
|
|
||||||
|
Full list of source specifications supported by the function are:
|
||||||
|
|
||||||
|
'distro': A NOP; i.e. it has no effect.
|
||||||
|
'proposed': the proposed deb spec [2] is wrtten to
|
||||||
|
/etc/apt/sources.list/proposed
|
||||||
|
'distro-proposed': adds <version>-proposed to the debs [2]
|
||||||
|
'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
|
||||||
|
'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
|
||||||
|
'http://....': add-apt-repository --yes http://...
|
||||||
|
'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
|
||||||
|
'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
|
||||||
|
optional staging version. If staging is used then the staging PPA [2]
|
||||||
|
with be used. If staging is NOT used then the cloud archive [3] will be
|
||||||
|
added, and the 'ubuntu-cloud-keyring' package will be added for the
|
||||||
|
current distro.
|
||||||
|
|
||||||
|
Otherwise the source is not recognised and this is logged to the juju log.
|
||||||
|
However, no error is raised, unless sys_error_on_exit is True.
|
||||||
|
|
||||||
|
[1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
|
||||||
|
where {} is replaced with the derived pocket name.
|
||||||
|
[2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
|
||||||
|
main universe multiverse restricted
|
||||||
|
where {} is replaced with the lsb_release codename (e.g. xenial)
|
||||||
|
[3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
|
||||||
|
to /etc/apt/sources.list.d/cloud-archive-list
|
||||||
|
|
||||||
@param key: A key to be added to the system's APT keyring and used
|
@param key: A key to be added to the system's APT keyring and used
|
||||||
to verify the signatures on packages. Ideally, this should be an
|
to verify the signatures on packages. Ideally, this should be an
|
||||||
ASCII format GPG public key including the block headers. A GPG key
|
ASCII format GPG public key including the block headers. A GPG key
|
||||||
@ -256,51 +349,142 @@ def add_source(source, key=None):
|
|||||||
available to retrieve the actual public key from a public keyserver
|
available to retrieve the actual public key from a public keyserver
|
||||||
placing your Juju environment at risk. ppa and cloud archive keys
|
placing your Juju environment at risk. ppa and cloud archive keys
|
||||||
are securely added automtically, so sould not be provided.
|
are securely added automtically, so sould not be provided.
|
||||||
|
|
||||||
|
@param fail_invalid: (boolean) if True, then the function raises a
|
||||||
|
SourceConfigError is there is no matching installation source.
|
||||||
|
|
||||||
|
@raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
|
||||||
|
valid pocket in CLOUD_ARCHIVE_POCKETS
|
||||||
"""
|
"""
|
||||||
|
_mapping = OrderedDict([
|
||||||
|
(r"^distro$", lambda: None), # This is a NOP
|
||||||
|
(r"^(?:proposed|distro-proposed)$", _add_proposed),
|
||||||
|
(r"^cloud-archive:(.*)$", _add_apt_repository),
|
||||||
|
(r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
|
||||||
|
(r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
|
||||||
|
(r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
|
||||||
|
(r"^cloud:(.*)$", _add_cloud_pocket),
|
||||||
|
(r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
|
||||||
|
])
|
||||||
if source is None:
|
if source is None:
|
||||||
log('Source is not present. Skipping')
|
source = ''
|
||||||
return
|
for r, fn in six.iteritems(_mapping):
|
||||||
|
m = re.match(r, source)
|
||||||
if (source.startswith('ppa:') or
|
if m:
|
||||||
source.startswith('http') or
|
# call the assoicated function with the captured groups
|
||||||
source.startswith('deb ') or
|
# raises SourceConfigError on error.
|
||||||
source.startswith('cloud-archive:')):
|
fn(*m.groups())
|
||||||
cmd = ['add-apt-repository', '--yes', source]
|
if key:
|
||||||
_run_with_retries(cmd)
|
try:
|
||||||
elif source.startswith('cloud:'):
|
import_key(key)
|
||||||
install(filter_installed_packages(['ubuntu-cloud-keyring']),
|
except GPGKeyError as e:
|
||||||
fatal=True)
|
raise SourceConfigError(str(e))
|
||||||
pocket = source.split(':')[-1]
|
break
|
||||||
if pocket not in CLOUD_ARCHIVE_POCKETS:
|
|
||||||
raise SourceConfigError(
|
|
||||||
'Unsupported cloud: source option %s' %
|
|
||||||
pocket)
|
|
||||||
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
|
|
||||||
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
|
|
||||||
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
|
|
||||||
elif source == 'proposed':
|
|
||||||
release = lsb_release()['DISTRIB_CODENAME']
|
|
||||||
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
|
|
||||||
apt.write(PROPOSED_POCKET.format(release))
|
|
||||||
elif source == 'distro':
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
log("Unknown source: {!r}".format(source))
|
# nothing matched. log an error and maybe sys.exit
|
||||||
|
err = "Unknown source: {!r}".format(source)
|
||||||
|
log(err)
|
||||||
|
if fail_invalid:
|
||||||
|
raise SourceConfigError(err)
|
||||||
|
|
||||||
if key:
|
|
||||||
if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
|
def _add_proposed():
|
||||||
with NamedTemporaryFile('w+') as key_file:
|
"""Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
|
||||||
key_file.write(key)
|
|
||||||
key_file.flush()
|
Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
|
||||||
key_file.seek(0)
|
the deb line.
|
||||||
subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
|
|
||||||
else:
|
For intel architecutres PROPOSED_POCKET is used for the release, but for
|
||||||
# Note that hkp: is in no way a secure protocol. Using a
|
other architectures PROPOSED_PORTS_POCKET is used for the release.
|
||||||
# GPG key id is pointless from a security POV unless you
|
"""
|
||||||
# absolutely trust your network and DNS.
|
release = lsb_release()['DISTRIB_CODENAME']
|
||||||
subprocess.check_call(['apt-key', 'adv', '--keyserver',
|
arch = platform.machine()
|
||||||
'hkp://keyserver.ubuntu.com:80', '--recv',
|
if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
|
||||||
key])
|
raise SourceConfigError("Arch {} not supported for (distro-)proposed"
|
||||||
|
.format(arch))
|
||||||
|
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
|
||||||
|
apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
|
||||||
|
|
||||||
|
|
||||||
|
def _add_apt_repository(spec):
|
||||||
|
"""Add the spec using add_apt_repository
|
||||||
|
|
||||||
|
:param spec: the parameter to pass to add_apt_repository
|
||||||
|
"""
|
||||||
|
_run_with_retries(['add-apt-repository', '--yes', spec])
|
||||||
|
|
||||||
|
|
||||||
|
def _add_cloud_pocket(pocket):
|
||||||
|
"""Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
|
||||||
|
|
||||||
|
Note that this overwrites the existing file if there is one.
|
||||||
|
|
||||||
|
This function also converts the simple pocket in to the actual pocket using
|
||||||
|
the CLOUD_ARCHIVE_POCKETS mapping.
|
||||||
|
|
||||||
|
:param pocket: string representing the pocket to add a deb spec for.
|
||||||
|
:raises: SourceConfigError if the cloud pocket doesn't exist or the
|
||||||
|
requested release doesn't match the current distro version.
|
||||||
|
"""
|
||||||
|
apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
|
||||||
|
fatal=True)
|
||||||
|
if pocket not in CLOUD_ARCHIVE_POCKETS:
|
||||||
|
raise SourceConfigError(
|
||||||
|
'Unsupported cloud: source option %s' %
|
||||||
|
pocket)
|
||||||
|
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
|
||||||
|
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
|
||||||
|
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
|
||||||
|
|
||||||
|
|
||||||
|
def _add_cloud_staging(cloud_archive_release, openstack_release):
|
||||||
|
"""Add the cloud staging repository which is in
|
||||||
|
ppa:ubuntu-cloud-archive/<openstack_release>-staging
|
||||||
|
|
||||||
|
This function checks that the cloud_archive_release matches the current
|
||||||
|
codename for the distro that charm is being installed on.
|
||||||
|
|
||||||
|
:param cloud_archive_release: string, codename for the release.
|
||||||
|
:param openstack_release: String, codename for the openstack release.
|
||||||
|
:raises: SourceConfigError if the cloud_archive_release doesn't match the
|
||||||
|
current version of the os.
|
||||||
|
"""
|
||||||
|
_verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
|
||||||
|
ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
|
||||||
|
cmd = 'add-apt-repository -y {}'.format(ppa)
|
||||||
|
_run_with_retries(cmd.split(' '))
|
||||||
|
|
||||||
|
|
||||||
|
def _add_cloud_distro_check(cloud_archive_release, openstack_release):
|
||||||
|
"""Add the cloud pocket, but also check the cloud_archive_release against
|
||||||
|
the current distro, and use the openstack_release as the full lookup.
|
||||||
|
|
||||||
|
This just calls _add_cloud_pocket() with the openstack_release as pocket
|
||||||
|
to get the correct cloud-archive.list for dpkg to work with.
|
||||||
|
|
||||||
|
:param cloud_archive_release:String, codename for the distro release.
|
||||||
|
:param openstack_release: String, spec for the release to look up in the
|
||||||
|
CLOUD_ARCHIVE_POCKETS
|
||||||
|
:raises: SourceConfigError if this is the wrong distro, or the pocket spec
|
||||||
|
doesn't exist.
|
||||||
|
"""
|
||||||
|
_verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
|
||||||
|
_add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_is_ubuntu_rel(release, os_release):
|
||||||
|
"""Verify that the release is in the same as the current ubuntu release.
|
||||||
|
|
||||||
|
:param release: String, lowercase for the release.
|
||||||
|
:param os_release: String, the os_release being asked for
|
||||||
|
:raises: SourceConfigError if the release is not the same as the ubuntu
|
||||||
|
release.
|
||||||
|
"""
|
||||||
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||||
|
if release != ubuntu_rel:
|
||||||
|
raise SourceConfigError(
|
||||||
|
'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
|
||||||
|
'version ({})'.format(release, os_release, ubuntu_rel))
|
||||||
|
|
||||||
|
|
||||||
def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
|
def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
|
||||||
@ -316,9 +500,12 @@ def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
|
|||||||
:param: cmd_env: dict: Environment variables to add to the command run.
|
:param: cmd_env: dict: Environment variables to add to the command run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = None
|
||||||
|
kwargs = {}
|
||||||
if cmd_env:
|
if cmd_env:
|
||||||
|
env = os.environ.copy()
|
||||||
env.update(cmd_env)
|
env.update(cmd_env)
|
||||||
|
kwargs['env'] = env
|
||||||
|
|
||||||
if not retry_message:
|
if not retry_message:
|
||||||
retry_message = "Failed executing '{}'".format(" ".join(cmd))
|
retry_message = "Failed executing '{}'".format(" ".join(cmd))
|
||||||
@ -330,7 +517,8 @@ def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
|
|||||||
retry_results = (None,) + retry_exitcodes
|
retry_results = (None,) + retry_exitcodes
|
||||||
while result in retry_results:
|
while result in retry_results:
|
||||||
try:
|
try:
|
||||||
result = subprocess.check_call(cmd, env=env)
|
# result = subprocess.check_call(cmd, env=env)
|
||||||
|
result = subprocess.check_call(cmd, **kwargs)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
retry_count = retry_count + 1
|
retry_count = retry_count + 1
|
||||||
if retry_count > max_retries:
|
if retry_count > max_retries:
|
||||||
@ -343,6 +531,7 @@ def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
|
|||||||
def _run_apt_command(cmd, fatal=False):
|
def _run_apt_command(cmd, fatal=False):
|
||||||
"""Run an apt command with optional retries.
|
"""Run an apt command with optional retries.
|
||||||
|
|
||||||
|
:param: cmd: str: The apt command to run.
|
||||||
:param: fatal: bool: Whether the command's output should be checked and
|
:param: fatal: bool: Whether the command's output should be checked and
|
||||||
retried.
|
retried.
|
||||||
"""
|
"""
|
||||||
|
@ -667,10 +667,12 @@ def ha_joined(relation_id=None):
|
|||||||
if iface is not None:
|
if iface is not None:
|
||||||
vip_key = 'res_ks_{}_vip'.format(iface)
|
vip_key = 'res_ks_{}_vip'.format(iface)
|
||||||
if vip_key in vip_group:
|
if vip_key in vip_group:
|
||||||
log("Resource '%s' (vip='%s') already exists in "
|
if vip not in resource_params[vip_key]:
|
||||||
"vip group - skipping" % (vip_key, vip),
|
vip_key = '{}_{}'.format(vip_key, vip_params)
|
||||||
WARNING)
|
else:
|
||||||
continue
|
log("Resource '%s' (vip='%s') already exists in "
|
||||||
|
"vip group - skipping" % (vip_key, vip), WARNING)
|
||||||
|
continue
|
||||||
|
|
||||||
vip_group.append(vip_key)
|
vip_group.append(vip_key)
|
||||||
resources[vip_key] = res_ks_vip
|
resources[vip_key] = res_ks_vip
|
||||||
@ -711,7 +713,10 @@ def ha_changed():
|
|||||||
if clustered:
|
if clustered:
|
||||||
log('Cluster configured, notifying other services and updating '
|
log('Cluster configured, notifying other services and updating '
|
||||||
'keystone endpoint configuration')
|
'keystone endpoint configuration')
|
||||||
update_all_identity_relation_units()
|
if is_ssl_cert_master():
|
||||||
|
update_all_identity_relation_units_force_sync()
|
||||||
|
else:
|
||||||
|
update_all_identity_relation_units()
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook('identity-admin-relation-changed')
|
@hooks.hook('identity-admin-relation-changed')
|
||||||
|
@ -34,7 +34,7 @@ from base64 import b64encode
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from charmhelpers.contrib.hahelpers.cluster import(
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
is_elected_leader,
|
is_elected_leader,
|
||||||
determine_api_port,
|
determine_api_port,
|
||||||
https,
|
https,
|
||||||
@ -105,6 +105,8 @@ from charmhelpers.core.hookenv import (
|
|||||||
DEBUG,
|
DEBUG,
|
||||||
INFO,
|
INFO,
|
||||||
WARNING,
|
WARNING,
|
||||||
|
ERROR,
|
||||||
|
is_leader,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
@ -1344,19 +1346,46 @@ def get_ssl_sync_request_units():
|
|||||||
|
|
||||||
def is_ssl_cert_master(votes=None):
|
def is_ssl_cert_master(votes=None):
|
||||||
"""Return True if this unit is ssl cert master."""
|
"""Return True if this unit is ssl cert master."""
|
||||||
master = None
|
|
||||||
for rid in relation_ids('cluster'):
|
|
||||||
master = relation_get(attribute='ssl-cert-master', rid=rid,
|
|
||||||
unit=local_unit())
|
|
||||||
|
|
||||||
if master == local_unit():
|
votes = votes or get_ssl_cert_master_votes()
|
||||||
votes = votes or get_ssl_cert_master_votes()
|
set_votes = set(votes)
|
||||||
if not peer_units() or (len(votes) == 1 and master in votes):
|
# Discard unknown votes
|
||||||
return True
|
if 'unknown' in set_votes:
|
||||||
|
set_votes.remove('unknown')
|
||||||
|
|
||||||
|
# This is the elected ssl-cert-master leader
|
||||||
|
if len(set_votes) == 1 and set_votes == set([local_unit()]):
|
||||||
|
log("This unit is the elected ssl-cert-master "
|
||||||
|
"{}".format(votes), level=DEBUG)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Contested election
|
||||||
|
if len(set_votes) > 1:
|
||||||
log("Did not get consensus from peers on who is ssl-cert-master "
|
log("Did not get consensus from peers on who is ssl-cert-master "
|
||||||
"(%s)" % (votes), level=INFO)
|
"{}".format(votes), level=DEBUG)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Neither the elected ssl-cert-master leader nor the juju leader
|
||||||
|
if not is_leader():
|
||||||
|
return False
|
||||||
|
# Only the juju elected leader continues
|
||||||
|
|
||||||
|
# Singleton
|
||||||
|
if not peer_units():
|
||||||
|
log("This unit is a singleton and thefore ssl-cert-master",
|
||||||
|
level=DEBUG)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Early in the process and juju leader
|
||||||
|
if not votes:
|
||||||
|
log("This unit is the juju leader and there are no votes yet, "
|
||||||
|
"becoming the ssl-cert-master",
|
||||||
|
level=DEBUG)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Should never reach here
|
||||||
|
log("Could not determine the ssl-cert-master. Missing edge case.",
|
||||||
|
level=ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,7 +154,32 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
}
|
}
|
||||||
super(KeystoneBasicDeployment, self)._configure_services(configs)
|
super(KeystoneBasicDeployment, self)._configure_services(configs)
|
||||||
|
|
||||||
|
def api_change_required(self, api_version):
|
||||||
|
if api_version == 2:
|
||||||
|
try:
|
||||||
|
self.keystone_v2.service_catalog.get_urls()
|
||||||
|
u.log.debug('Already at required api version {}'
|
||||||
|
''.format(api_version))
|
||||||
|
return False
|
||||||
|
except (AttributeError, ksauth1_exceptions.http.Unauthorized):
|
||||||
|
u.log.debug('Change to api version {} required'
|
||||||
|
''.format(api_version))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.keystone_v3.service_catalog.get_urls()
|
||||||
|
u.log.debug('Already at required api version {}'
|
||||||
|
''.format(api_version))
|
||||||
|
return False
|
||||||
|
except (AttributeError, ksauth1_exceptions.http.Unauthorized):
|
||||||
|
u.log.debug('Change to api version {} required'
|
||||||
|
''.format(api_version))
|
||||||
|
return True
|
||||||
|
|
||||||
def set_api_version(self, api_version):
|
def set_api_version(self, api_version):
|
||||||
|
# Avoid costly settings if we are already at the correct api_version
|
||||||
|
if not self.api_change_required(api_version):
|
||||||
|
return True
|
||||||
u.log.debug('Setting preferred-api-version={}'.format(api_version))
|
u.log.debug('Setting preferred-api-version={}'.format(api_version))
|
||||||
se_rels = []
|
se_rels = []
|
||||||
for i in range(0, self.keystone_num_units):
|
for i in range(0, self.keystone_num_units):
|
||||||
@ -204,10 +229,10 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
tenant_id=tenant.id,
|
tenant_id=tenant.id,
|
||||||
email='demo@demo.com')
|
email='demo@demo.com')
|
||||||
|
|
||||||
# Authenticate keystone demo
|
# Authenticate keystone demo
|
||||||
self.keystone_demo = u.authenticate_keystone_user(
|
self.keystone_demo = u.authenticate_keystone_user(
|
||||||
self.keystone_v2, user=self.demo_user,
|
self.keystone_v2, user=self.demo_user,
|
||||||
password='password', tenant=self.demo_tenant)
|
password='password', tenant=self.demo_tenant)
|
||||||
|
|
||||||
def create_users_v3(self):
|
def create_users_v3(self):
|
||||||
# Create a demo tenant/role/user
|
# Create a demo tenant/role/user
|
||||||
@ -292,9 +317,6 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
'shared-db',
|
'shared-db',
|
||||||
'percona-cluster:shared-db')['private-address']
|
'percona-cluster:shared-db')['private-address']
|
||||||
self.set_api_version(2)
|
self.set_api_version(2)
|
||||||
# Authenticate keystone admin
|
|
||||||
self.keystone_v2 = self.get_keystone_client(api_version=2)
|
|
||||||
self.keystone_v3 = self.get_keystone_client(api_version=3)
|
|
||||||
self.create_users_v2()
|
self.create_users_v2()
|
||||||
|
|
||||||
def test_100_services(self):
|
def test_100_services(self):
|
||||||
@ -439,23 +461,15 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
u.log.info('Skipping test, {} < mitaka'.format(os_release))
|
u.log.info('Skipping test, {} < mitaka'.format(os_release))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_112_keystone_tenants(self):
|
def test_112_keystone_list_resources(self):
|
||||||
if self.is_liberty_or_newer():
|
if self.is_mitaka_or_newer():
|
||||||
self.set_api_version(3)
|
self.set_api_version(3)
|
||||||
self.validate_keystone_tenants(self.keystone_v3)
|
self.validate_keystone_tenants(self.keystone_v3)
|
||||||
|
|
||||||
def test_114_keystone_tenants(self):
|
|
||||||
if self.is_liberty_or_newer():
|
|
||||||
self.set_api_version(3)
|
|
||||||
self.validate_keystone_roles(self.keystone_v3)
|
self.validate_keystone_roles(self.keystone_v3)
|
||||||
|
|
||||||
def test_116_keystone_users(self):
|
|
||||||
if self.is_liberty_or_newer():
|
|
||||||
self.set_api_version(3)
|
|
||||||
self.validate_keystone_users(self.keystone_v3)
|
self.validate_keystone_users(self.keystone_v3)
|
||||||
|
|
||||||
def test_118_keystone_users(self):
|
def test_118_keystone_create_users(self):
|
||||||
if self.is_liberty_or_newer():
|
if self.is_mitaka_or_newer():
|
||||||
self.set_api_version(3)
|
self.set_api_version(3)
|
||||||
self.create_users_v3()
|
self.create_users_v3()
|
||||||
actual_user = self.find_keystone_v3_user(self.keystone_v3,
|
actual_user = self.find_keystone_v3_user(self.keystone_v3,
|
||||||
@ -476,7 +490,7 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
assert expect[key] == getattr(actual_user, key)
|
assert expect[key] == getattr(actual_user, key)
|
||||||
|
|
||||||
def test_120_keystone_domains(self):
|
def test_120_keystone_domains(self):
|
||||||
if self.is_liberty_or_newer():
|
if self.is_mitaka_or_newer():
|
||||||
self.set_api_version(3)
|
self.set_api_version(3)
|
||||||
self.create_users_v3()
|
self.create_users_v3()
|
||||||
actual_domain = self.keystone_v3.domains.find(
|
actual_domain = self.keystone_v3.domains.find(
|
||||||
@ -906,7 +920,7 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
# option to fix this.
|
# option to fix this.
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.is_liberty_or_newer():
|
if self.is_mitaka_or_newer():
|
||||||
timeout = int(os.environ.get('AMULET_SETUP_TIMEOUT', 900))
|
timeout = int(os.environ.get('AMULET_SETUP_TIMEOUT', 900))
|
||||||
self.set_api_version(3)
|
self.set_api_version(3)
|
||||||
self._auto_wait_for_status(
|
self._auto_wait_for_status(
|
||||||
|
@ -14,6 +14,11 @@
|
|||||||
|
|
||||||
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
||||||
# only standard libraries.
|
# only standard libraries.
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -34,3 +39,59 @@ except ImportError:
|
|||||||
else:
|
else:
|
||||||
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
||||||
import yaml # flake8: noqa
|
import yaml # flake8: noqa
|
||||||
|
|
||||||
|
|
||||||
|
# Holds a list of mapping of mangled function names that have been deprecated
|
||||||
|
# using the @deprecate decorator below. This is so that the warning is only
|
||||||
|
# printed once for each usage of the function.
|
||||||
|
__deprecated_functions = {}
|
||||||
|
|
||||||
|
|
||||||
|
def deprecate(warning, date=None, log=None):
|
||||||
|
"""Add a deprecation warning the first time the function is used.
|
||||||
|
The date, which is a string in semi-ISO8660 format indicate the year-month
|
||||||
|
that the function is officially going to be removed.
|
||||||
|
|
||||||
|
usage:
|
||||||
|
|
||||||
|
@deprecate('use core/fetch/add_source() instead', '2017-04')
|
||||||
|
def contributed_add_source_thing(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
And it then prints to the log ONCE that the function is deprecated.
|
||||||
|
The reason for passing the logging function (log) is so that hookenv.log
|
||||||
|
can be used for a charm if needed.
|
||||||
|
|
||||||
|
:param warning: String to indicat where it has moved ot.
|
||||||
|
:param date: optional sting, in YYYY-MM format to indicate when the
|
||||||
|
function will definitely (probably) be removed.
|
||||||
|
:param log: The log function to call to log. If not, logs to stdout
|
||||||
|
"""
|
||||||
|
def wrap(f):
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapped_f(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
module = inspect.getmodule(f)
|
||||||
|
file = inspect.getsourcefile(f)
|
||||||
|
lines = inspect.getsourcelines(f)
|
||||||
|
f_name = "{}-{}-{}..{}-{}".format(
|
||||||
|
module.__name__, file, lines[0], lines[-1], f.__name__)
|
||||||
|
except (IOError, TypeError):
|
||||||
|
# assume it was local, so just use the name of the function
|
||||||
|
f_name = f.__name__
|
||||||
|
if f_name not in __deprecated_functions:
|
||||||
|
__deprecated_functions[f_name] = True
|
||||||
|
s = "DEPRECATION WARNING: Function {} is being removed".format(
|
||||||
|
f.__name__)
|
||||||
|
if date:
|
||||||
|
s = "{} on/around {}".format(s, date)
|
||||||
|
if warning:
|
||||||
|
s = "{} : {}".format(s, warning)
|
||||||
|
if log:
|
||||||
|
log(s)
|
||||||
|
else:
|
||||||
|
print(s)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapped_f
|
||||||
|
return wrap
|
||||||
|
@ -25,9 +25,12 @@ import urlparse
|
|||||||
import cinderclient.v1.client as cinder_client
|
import cinderclient.v1.client as cinder_client
|
||||||
import glanceclient.v1.client as glance_client
|
import glanceclient.v1.client as glance_client
|
||||||
import heatclient.v1.client as heat_client
|
import heatclient.v1.client as heat_client
|
||||||
import keystoneclient.v2_0 as keystone_client
|
from keystoneclient.v2_0 import client as keystone_client
|
||||||
from keystoneclient.auth.identity import v3 as keystone_id_v3
|
from keystoneauth1.identity import (
|
||||||
from keystoneclient import session as keystone_session
|
v3,
|
||||||
|
v2,
|
||||||
|
)
|
||||||
|
from keystoneauth1 import session as keystone_session
|
||||||
from keystoneclient.v3 import client as keystone_client_v3
|
from keystoneclient.v3 import client as keystone_client_v3
|
||||||
from novaclient import exceptions
|
from novaclient import exceptions
|
||||||
|
|
||||||
@ -368,12 +371,20 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
port)
|
port)
|
||||||
if not api_version or api_version == 2:
|
if not api_version or api_version == 2:
|
||||||
ep = base_ep + "/v2.0"
|
ep = base_ep + "/v2.0"
|
||||||
return keystone_client.Client(username=username, password=password,
|
auth = v2.Password(
|
||||||
tenant_name=project_name,
|
username=username,
|
||||||
auth_url=ep)
|
password=password,
|
||||||
|
tenant_name=project_name,
|
||||||
|
auth_url=ep
|
||||||
|
)
|
||||||
|
sess = keystone_session.Session(auth=auth)
|
||||||
|
client = keystone_client.Client(session=sess)
|
||||||
|
# This populates the client.service_catalog
|
||||||
|
client.auth_ref = auth.get_access(sess)
|
||||||
|
return client
|
||||||
else:
|
else:
|
||||||
ep = base_ep + "/v3"
|
ep = base_ep + "/v3"
|
||||||
auth = keystone_id_v3.Password(
|
auth = v3.Password(
|
||||||
user_domain_name=user_domain_name,
|
user_domain_name=user_domain_name,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
@ -382,36 +393,45 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
auth_url=ep
|
auth_url=ep
|
||||||
)
|
)
|
||||||
return keystone_client_v3.Client(
|
sess = keystone_session.Session(auth=auth)
|
||||||
session=keystone_session.Session(auth=auth)
|
client = keystone_client_v3.Client(session=sess)
|
||||||
)
|
# This populates the client.service_catalog
|
||||||
|
client.auth_ref = auth.get_access(sess)
|
||||||
|
return client
|
||||||
|
|
||||||
def authenticate_keystone_admin(self, keystone_sentry, user, password,
|
def authenticate_keystone_admin(self, keystone_sentry, user, password,
|
||||||
tenant=None, api_version=None,
|
tenant=None, api_version=None,
|
||||||
keystone_ip=None):
|
keystone_ip=None, user_domain_name=None,
|
||||||
|
project_domain_name=None,
|
||||||
|
project_name=None):
|
||||||
"""Authenticates admin user with the keystone admin endpoint."""
|
"""Authenticates admin user with the keystone admin endpoint."""
|
||||||
self.log.debug('Authenticating keystone admin...')
|
self.log.debug('Authenticating keystone admin...')
|
||||||
if not keystone_ip:
|
if not keystone_ip:
|
||||||
keystone_ip = keystone_sentry.info['public-address']
|
keystone_ip = keystone_sentry.info['public-address']
|
||||||
|
|
||||||
user_domain_name = None
|
# To support backward compatibility usage of this function
|
||||||
domain_name = None
|
if not project_name:
|
||||||
if api_version == 3:
|
project_name = tenant
|
||||||
|
if api_version == 3 and not user_domain_name:
|
||||||
user_domain_name = 'admin_domain'
|
user_domain_name = 'admin_domain'
|
||||||
domain_name = user_domain_name
|
if api_version == 3 and not project_domain_name:
|
||||||
|
project_domain_name = 'admin_domain'
|
||||||
|
if api_version == 3 and not project_name:
|
||||||
|
project_name = 'admin'
|
||||||
|
|
||||||
return self.authenticate_keystone(keystone_ip, user, password,
|
return self.authenticate_keystone(
|
||||||
project_name=tenant,
|
keystone_ip, user, password,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
user_domain_name=user_domain_name,
|
user_domain_name=user_domain_name,
|
||||||
domain_name=domain_name,
|
project_domain_name=project_domain_name,
|
||||||
admin_port=True)
|
project_name=project_name,
|
||||||
|
admin_port=True)
|
||||||
|
|
||||||
def authenticate_keystone_user(self, keystone, user, password, tenant):
|
def authenticate_keystone_user(self, keystone, user, password, tenant):
|
||||||
"""Authenticates a regular user with the keystone public endpoint."""
|
"""Authenticates a regular user with the keystone public endpoint."""
|
||||||
self.log.debug('Authenticating keystone user ({})...'.format(user))
|
self.log.debug('Authenticating keystone user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
keystone_ip = urlparse.urlparse(ep).hostname
|
keystone_ip = urlparse.urlparse(ep).hostname
|
||||||
|
|
||||||
return self.authenticate_keystone(keystone_ip, user, password,
|
return self.authenticate_keystone(keystone_ip, user, password,
|
||||||
@ -421,22 +441,32 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
"""Authenticates admin user with glance."""
|
"""Authenticates admin user with glance."""
|
||||||
self.log.debug('Authenticating glance admin...')
|
self.log.debug('Authenticating glance admin...')
|
||||||
ep = keystone.service_catalog.url_for(service_type='image',
|
ep = keystone.service_catalog.url_for(service_type='image',
|
||||||
endpoint_type='adminURL')
|
interface='adminURL')
|
||||||
return glance_client.Client(ep, token=keystone.auth_token)
|
if keystone.session:
|
||||||
|
return glance_client.Client(ep, session=keystone.session)
|
||||||
|
else:
|
||||||
|
return glance_client.Client(ep, token=keystone.auth_token)
|
||||||
|
|
||||||
def authenticate_heat_admin(self, keystone):
|
def authenticate_heat_admin(self, keystone):
|
||||||
"""Authenticates the admin user with heat."""
|
"""Authenticates the admin user with heat."""
|
||||||
self.log.debug('Authenticating heat admin...')
|
self.log.debug('Authenticating heat admin...')
|
||||||
ep = keystone.service_catalog.url_for(service_type='orchestration',
|
ep = keystone.service_catalog.url_for(service_type='orchestration',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
return heat_client.Client(endpoint=ep, token=keystone.auth_token)
|
if keystone.session:
|
||||||
|
return heat_client.Client(endpoint=ep, session=keystone.session)
|
||||||
|
else:
|
||||||
|
return heat_client.Client(endpoint=ep, token=keystone.auth_token)
|
||||||
|
|
||||||
def authenticate_nova_user(self, keystone, user, password, tenant):
|
def authenticate_nova_user(self, keystone, user, password, tenant):
|
||||||
"""Authenticates a regular user with nova-api."""
|
"""Authenticates a regular user with nova-api."""
|
||||||
self.log.debug('Authenticating nova user ({})...'.format(user))
|
self.log.debug('Authenticating nova user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
if novaclient.__version__[0] >= "7":
|
if keystone.session:
|
||||||
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
|
session=keystone.session,
|
||||||
|
auth_url=ep)
|
||||||
|
elif novaclient.__version__[0] >= "7":
|
||||||
return nova_client.Client(NOVA_CLIENT_VERSION,
|
return nova_client.Client(NOVA_CLIENT_VERSION,
|
||||||
username=user, password=password,
|
username=user, password=password,
|
||||||
project_name=tenant, auth_url=ep)
|
project_name=tenant, auth_url=ep)
|
||||||
@ -449,12 +479,15 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
"""Authenticates a regular user with swift api."""
|
"""Authenticates a regular user with swift api."""
|
||||||
self.log.debug('Authenticating swift user ({})...'.format(user))
|
self.log.debug('Authenticating swift user ({})...'.format(user))
|
||||||
ep = keystone.service_catalog.url_for(service_type='identity',
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
endpoint_type='publicURL')
|
interface='publicURL')
|
||||||
return swiftclient.Connection(authurl=ep,
|
if keystone.session:
|
||||||
user=user,
|
return swiftclient.Connection(session=keystone.session)
|
||||||
key=password,
|
else:
|
||||||
tenant_name=tenant,
|
return swiftclient.Connection(authurl=ep,
|
||||||
auth_version='2.0')
|
user=user,
|
||||||
|
key=password,
|
||||||
|
tenant_name=tenant,
|
||||||
|
auth_version='2.0')
|
||||||
|
|
||||||
def create_flavor(self, nova, name, ram, vcpus, disk, flavorid="auto",
|
def create_flavor(self, nova, name, ram, vcpus, disk, flavorid="auto",
|
||||||
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
|
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
|
||||||
|
@ -202,6 +202,27 @@ def service_name():
|
|||||||
return local_unit().split('/')[0]
|
return local_unit().split('/')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def principal_unit():
|
||||||
|
"""Returns the principal unit of this unit, otherwise None"""
|
||||||
|
# Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT
|
||||||
|
principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None)
|
||||||
|
# If it's empty, then this unit is the principal
|
||||||
|
if principal_unit == '':
|
||||||
|
return os.environ['JUJU_UNIT_NAME']
|
||||||
|
elif principal_unit is not None:
|
||||||
|
return principal_unit
|
||||||
|
# For Juju 2.1 and below, let's try work out the principle unit by
|
||||||
|
# the various charms' metadata.yaml.
|
||||||
|
for reltype in relation_types():
|
||||||
|
for rid in relation_ids(reltype):
|
||||||
|
for unit in related_units(rid):
|
||||||
|
md = _metadata_unit(unit)
|
||||||
|
subordinate = md.pop('subordinate', None)
|
||||||
|
if not subordinate:
|
||||||
|
return unit
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def remote_service_name(relid=None):
|
def remote_service_name(relid=None):
|
||||||
"""The remote service name for a given relation-id (or the current relation)"""
|
"""The remote service name for a given relation-id (or the current relation)"""
|
||||||
@ -478,6 +499,21 @@ def metadata():
|
|||||||
return yaml.safe_load(md)
|
return yaml.safe_load(md)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_unit(unit):
|
||||||
|
"""Given the name of a unit (e.g. apache2/0), get the unit charm's
|
||||||
|
metadata.yaml. Very similar to metadata() but allows us to inspect
|
||||||
|
other units. Unit needs to be co-located, such as a subordinate or
|
||||||
|
principal/primary.
|
||||||
|
|
||||||
|
:returns: metadata.yaml as a python object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
basedir = os.sep.join(charm_dir().split(os.sep)[:-2])
|
||||||
|
unitdir = 'unit-{}'.format(unit.replace(os.sep, '-'))
|
||||||
|
with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md:
|
||||||
|
return yaml.safe_load(md)
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def relation_types():
|
def relation_types():
|
||||||
"""Get a list of relation types supported by this charm"""
|
"""Get a list of relation types supported by this charm"""
|
||||||
@ -753,6 +789,9 @@ class Hooks(object):
|
|||||||
|
|
||||||
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')
|
||||||
|
if d is not None:
|
||||||
|
return d
|
||||||
return os.environ.get('CHARM_DIR')
|
return os.environ.get('CHARM_DIR')
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +191,7 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
|
|||||||
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
|
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
|
||||||
sysv_file = os.path.join(initd_dir, service_name)
|
sysv_file = os.path.join(initd_dir, service_name)
|
||||||
if init_is_systemd():
|
if init_is_systemd():
|
||||||
|
service('disable', service_name)
|
||||||
service('mask', service_name)
|
service('mask', service_name)
|
||||||
elif os.path.exists(upstart_file):
|
elif os.path.exists(upstart_file):
|
||||||
override_path = os.path.join(
|
override_path = os.path.join(
|
||||||
@ -225,6 +226,7 @@ def service_resume(service_name, init_dir="/etc/init",
|
|||||||
sysv_file = os.path.join(initd_dir, service_name)
|
sysv_file = os.path.join(initd_dir, service_name)
|
||||||
if init_is_systemd():
|
if init_is_systemd():
|
||||||
service('unmask', service_name)
|
service('unmask', service_name)
|
||||||
|
service('enable', service_name)
|
||||||
elif os.path.exists(upstart_file):
|
elif os.path.exists(upstart_file):
|
||||||
override_path = os.path.join(
|
override_path = os.path.join(
|
||||||
init_dir, '{}.override'.format(service_name))
|
init_dir, '{}.override'.format(service_name))
|
||||||
|
@ -58,6 +58,10 @@ class TestKeystoneContexts(CharmTestCase):
|
|||||||
self.assertTrue(mock_ensure_permissions.called)
|
self.assertTrue(mock_ensure_permissions.called)
|
||||||
self.assertFalse(mock_get_ca.called)
|
self.assertFalse(mock_get_ca.called)
|
||||||
|
|
||||||
|
@patch('charmhelpers.contrib.hahelpers.cluster.relation_ids')
|
||||||
|
@patch('charmhelpers.contrib.openstack.ip.unit_get')
|
||||||
|
@patch('charmhelpers.contrib.openstack.ip.service_name')
|
||||||
|
@patch('charmhelpers.contrib.openstack.ip.config')
|
||||||
@patch('keystone_utils.determine_ports')
|
@patch('keystone_utils.determine_ports')
|
||||||
@patch('keystone_utils.is_ssl_cert_master')
|
@patch('keystone_utils.is_ssl_cert_master')
|
||||||
@patch('charmhelpers.contrib.openstack.context.config')
|
@patch('charmhelpers.contrib.openstack.context.config')
|
||||||
@ -73,14 +77,21 @@ class TestKeystoneContexts(CharmTestCase):
|
|||||||
mock_is_clustered,
|
mock_is_clustered,
|
||||||
mock_config,
|
mock_config,
|
||||||
mock_is_ssl_cert_master,
|
mock_is_ssl_cert_master,
|
||||||
mock_determine_ports):
|
mock_determine_ports,
|
||||||
|
mock_ip_config,
|
||||||
|
mock_service_name,
|
||||||
|
mock_ip_unit_get,
|
||||||
|
mock_rel_ids,
|
||||||
|
):
|
||||||
mock_is_ssl_cert_master.return_value = True
|
mock_is_ssl_cert_master.return_value = True
|
||||||
mock_https.return_value = True
|
mock_https.return_value = True
|
||||||
mock_unit_get.return_value = '1.2.3.4'
|
mock_unit_get.return_value = '1.2.3.4'
|
||||||
|
mock_ip_unit_get.return_value = '1.2.3.4'
|
||||||
mock_determine_api_port.return_value = '12'
|
mock_determine_api_port.return_value = '12'
|
||||||
mock_determine_apache_port.return_value = '34'
|
mock_determine_apache_port.return_value = '34'
|
||||||
mock_is_clustered.return_value = False
|
mock_is_clustered.return_value = False
|
||||||
mock_config.return_value = None
|
mock_config.return_value = None
|
||||||
|
mock_ip_config.return_value = None
|
||||||
mock_determine_ports.return_value = ['12']
|
mock_determine_ports.return_value = ['12']
|
||||||
|
|
||||||
ctxt = context.ApacheSSLContext()
|
ctxt = context.ApacheSSLContext()
|
||||||
@ -150,33 +161,6 @@ class TestKeystoneContexts(CharmTestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('charmhelpers.contrib.openstack.context.log')
|
|
||||||
@patch('charmhelpers.contrib.openstack.context.config')
|
|
||||||
@patch('charmhelpers.contrib.openstack.context.unit_get')
|
|
||||||
@patch('charmhelpers.contrib.openstack.context.is_clustered')
|
|
||||||
@patch('charmhelpers.contrib.network.ip.get_address_in_network')
|
|
||||||
def test_canonical_names_without_network_splits(self,
|
|
||||||
mock_get_address,
|
|
||||||
mock_is_clustered,
|
|
||||||
mock_unit_get,
|
|
||||||
mock_config,
|
|
||||||
mock_log):
|
|
||||||
NET_CONFIG = {'vip': '10.0.3.1 10.0.3.2',
|
|
||||||
'os-internal-network': None,
|
|
||||||
'os-admin-network': None,
|
|
||||||
'os-public-network': None}
|
|
||||||
|
|
||||||
mock_unit_get.return_value = '10.0.3.10'
|
|
||||||
mock_is_clustered.return_value = True
|
|
||||||
config = {}
|
|
||||||
config.update(NET_CONFIG)
|
|
||||||
mock_config.side_effect = lambda key: config[key]
|
|
||||||
apache = context.ApacheSSLContext()
|
|
||||||
apache.canonical_names()
|
|
||||||
msg = "Multiple networks configured but net_type" \
|
|
||||||
" is None (os-public-network)."
|
|
||||||
mock_log.assert_called_with(msg, level="WARNING")
|
|
||||||
|
|
||||||
@patch.object(context, 'config')
|
@patch.object(context, 'config')
|
||||||
def test_keystone_logger_context(self, mock_config):
|
def test_keystone_logger_context(self, mock_config):
|
||||||
ctxt = context.KeystoneLoggingContext()
|
ctxt = context.KeystoneLoggingContext()
|
||||||
|
@ -813,7 +813,7 @@ class KeystoneRelationTests(CharmTestCase):
|
|||||||
|
|
||||||
def test_ha_joined_duplicate_vip_key(self):
|
def test_ha_joined_duplicate_vip_key(self):
|
||||||
self.get_hacluster_config.return_value = {
|
self.get_hacluster_config.return_value = {
|
||||||
'vip': '10.10.10.10 10.10.10.11',
|
'vip': '10.10.10.10 10.10.10.10',
|
||||||
'ha-bindiface': 'em0',
|
'ha-bindiface': 'em0',
|
||||||
'ha-mcastport': '8080'
|
'ha-mcastport': '8080'
|
||||||
}
|
}
|
||||||
@ -835,6 +835,33 @@ class KeystoneRelationTests(CharmTestCase):
|
|||||||
}
|
}
|
||||||
self.relation_set.assert_called_with(**args)
|
self.relation_set.assert_called_with(**args)
|
||||||
|
|
||||||
|
def test_ha_joined_dual_stack_vips(self):
|
||||||
|
self.get_hacluster_config.return_value = {
|
||||||
|
'vip': '10.10.10.10 2001:db8::abc',
|
||||||
|
'ha-bindiface': 'em0',
|
||||||
|
'ha-mcastport': '8080'
|
||||||
|
}
|
||||||
|
self.get_iface_for_address.return_value = 'em1'
|
||||||
|
self.get_netmask_for_address.return_value = '255.255.255.0'
|
||||||
|
hooks.ha_joined()
|
||||||
|
args = {
|
||||||
|
'relation_id': None,
|
||||||
|
'corosync_bindiface': 'em0',
|
||||||
|
'corosync_mcastport': '8080',
|
||||||
|
'init_services': {'res_ks_haproxy': 'haproxy'},
|
||||||
|
'resources': {'res_ks_em1_vip': 'ocf:heartbeat:IPaddr2',
|
||||||
|
'res_ks_em1_vip_ipv6addr': 'ocf:heartbeat:IPv6addr',
|
||||||
|
'res_ks_haproxy': 'lsb:haproxy'},
|
||||||
|
'resource_params': {
|
||||||
|
'res_ks_em1_vip': 'params ip="10.10.10.10"'
|
||||||
|
' cidr_netmask="255.255.255.0" nic="em1"',
|
||||||
|
'res_ks_em1_vip_ipv6addr': 'params ipv6addr="2001:db8::abc"'
|
||||||
|
' cidr_netmask="255.255.255.0" nic="em1"',
|
||||||
|
'res_ks_haproxy': 'op monitor interval="5s"'},
|
||||||
|
'clones': {'cl_ks_haproxy': 'res_ks_haproxy'}
|
||||||
|
}
|
||||||
|
self.relation_set.assert_called_with(**args)
|
||||||
|
|
||||||
def test_ha_joined_no_bound_ip(self):
|
def test_ha_joined_no_bound_ip(self):
|
||||||
self.get_hacluster_config.return_value = {
|
self.get_hacluster_config.return_value = {
|
||||||
'vip': '10.10.10.10',
|
'vip': '10.10.10.10',
|
||||||
@ -937,7 +964,8 @@ class KeystoneRelationTests(CharmTestCase):
|
|||||||
self.assertTrue(configs.write_all.called)
|
self.assertTrue(configs.write_all.called)
|
||||||
self.assertFalse(mock_synchronize_ca.called)
|
self.assertFalse(mock_synchronize_ca.called)
|
||||||
|
|
||||||
@patch.object(hooks, 'update_all_identity_relation_units')
|
@patch.object(hooks, 'is_ssl_cert_master')
|
||||||
|
@patch.object(hooks, 'update_all_identity_relation_units_force_sync')
|
||||||
@patch.object(hooks, 'is_db_initialised')
|
@patch.object(hooks, 'is_db_initialised')
|
||||||
@patch('keystone_utils.log')
|
@patch('keystone_utils.log')
|
||||||
@patch('keystone_utils.ensure_ssl_cert_master')
|
@patch('keystone_utils.ensure_ssl_cert_master')
|
||||||
@ -948,7 +976,7 @@ class KeystoneRelationTests(CharmTestCase):
|
|||||||
mock_ensure_ssl_cert_master,
|
mock_ensure_ssl_cert_master,
|
||||||
mock_log,
|
mock_log,
|
||||||
mock_is_db_initialised,
|
mock_is_db_initialised,
|
||||||
update):
|
update, cert_master):
|
||||||
mock_is_db_initialised.return_value = True
|
mock_is_db_initialised.return_value = True
|
||||||
self.is_db_ready.return_value = True
|
self.is_db_ready.return_value = True
|
||||||
mock_ensure_ssl_cert_master.return_value = False
|
mock_ensure_ssl_cert_master.return_value = False
|
||||||
@ -956,6 +984,7 @@ class KeystoneRelationTests(CharmTestCase):
|
|||||||
self.is_elected_leader.return_value = True
|
self.is_elected_leader.return_value = True
|
||||||
self.relation_ids.return_value = ['identity-service:0']
|
self.relation_ids.return_value = ['identity-service:0']
|
||||||
self.related_units.return_value = ['unit/0']
|
self.related_units.return_value = ['unit/0']
|
||||||
|
cert_master.return_value = True
|
||||||
|
|
||||||
hooks.ha_changed()
|
hooks.ha_changed()
|
||||||
self.assertTrue(configs.write_all.called)
|
self.assertTrue(configs.write_all.called)
|
||||||
|
@ -67,6 +67,7 @@ TO_PATCH = [
|
|||||||
'time',
|
'time',
|
||||||
'pwgen',
|
'pwgen',
|
||||||
'os_application_version_set',
|
'os_application_version_set',
|
||||||
|
'is_leader',
|
||||||
]
|
]
|
||||||
|
|
||||||
openstack_origin_git = \
|
openstack_origin_git = \
|
||||||
|
Loading…
Reference in New Issue
Block a user