Setup Ceph default RBD features

Nova-lxd requires that our Ceph images only contain the features
supported by the kernel RBD driver, and a discussion on the dev mailing
list suggests that 1 should work fine as the driver level

This commit contains a charmhelpers sync to bring in the new
flag to support configuration sent from the ceph charms.

Change-Id: Ifb20000fb8ec7c54e752fdb3acdc16043637a81d
This commit is contained in:
Chris MacNaughton 2017-07-27 10:37:07 +02:00
parent ce7f164e7f
commit 3c90647581
19 changed files with 583 additions and 46 deletions

View File

@ -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

View File

@ -243,11 +243,11 @@ 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,
universal_newlines=True)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return True return True
if six.PY3:
result = result.decode('UTF-8')
return "net.ipv6.conf.all.disable_ipv6 = 1" in result return "net.ipv6.conf.all.disable_ipv6 = 1" in result

View File

@ -97,6 +97,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 +245,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 +516,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)
@ -1397,6 +1407,18 @@ 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,
},
} }
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'):

View File

@ -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 -%}

View File

@ -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

View File

@ -0,0 +1,8 @@
{% if transport_url -%}
[oslo_messaging_notifications]
driver = messagingv2
transport_url = {{ transport_url }}
{% if notification_topics -%}
topics = {{ notification_topics }}
{% endif -%}
{% endif -%}

View File

@ -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
@ -90,6 +91,13 @@ from charmhelpers.fetch import (
GPGKeyError, 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
@ -327,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
@ -397,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()
@ -613,6 +636,9 @@ def openstack_upgrade_available(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)
@ -1863,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.
@ -1874,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
@ -1902,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)",
@ -1915,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]
@ -1982,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']))

View 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=(',', ': '))

View File

@ -1372,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 {}

View File

@ -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"""

View File

@ -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

View File

@ -139,7 +139,7 @@ CLOUD_ARCHIVE_POCKETS = {
'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',
@ -147,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',
@ -155,13 +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):
@ -364,6 +364,7 @@ def add_source(source, key=None, fail_invalid=False):
(r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging), (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
(r"^cloud:(.*)-(.*)$", _add_cloud_distro_check), (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
(r"^cloud:(.*)$", _add_cloud_pocket), (r"^cloud:(.*)$", _add_cloud_pocket),
(r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
]) ])
if source is None: if source is None:
source = '' source = ''

View File

@ -243,11 +243,11 @@ 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,
universal_newlines=True)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return True return True
if six.PY3:
result = result.decode('UTF-8')
return "net.ipv6.conf.all.disable_ipv6 = 1" in result return "net.ipv6.conf.all.disable_ipv6 = 1" in result

View File

@ -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
@ -90,6 +91,13 @@ from charmhelpers.fetch import (
GPGKeyError, 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
@ -327,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
@ -397,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()
@ -613,6 +636,9 @@ def openstack_upgrade_available(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)
@ -1863,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.
@ -1874,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
@ -1902,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)",
@ -1915,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]
@ -1982,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']))

View 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=(',', ': '))

View File

@ -1372,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 {}

View File

@ -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"""

View File

@ -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

View File

@ -139,7 +139,7 @@ CLOUD_ARCHIVE_POCKETS = {
'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',
@ -147,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',
@ -155,13 +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):
@ -364,6 +364,7 @@ def add_source(source, key=None, fail_invalid=False):
(r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging), (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
(r"^cloud:(.*)-(.*)$", _add_cloud_distro_check), (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
(r"^cloud:(.*)$", _add_cloud_pocket), (r"^cloud:(.*)$", _add_cloud_pocket),
(r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
]) ])
if source is None: if source is None:
source = '' source = ''