c-h sync. unit test updates for sync

This commit is contained in:
Billy Olsen 2015-06-04 16:06:40 -07:00
parent 195271dcce
commit 615c48006d
14 changed files with 383 additions and 76 deletions

View File

@ -52,6 +52,8 @@ from charmhelpers.core.strutils import (
bool_from_string, bool_from_string,
) )
DC_RESOURCE_NAME = 'DC'
class HAIncompleteConfig(Exception): class HAIncompleteConfig(Exception):
pass pass
@ -95,6 +97,27 @@ def is_clustered():
return False return False
def is_crm_dc():
"""
Determine leadership by querying the pacemaker Designated Controller
"""
cmd = ['crm', 'status']
try:
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
if not isinstance(status, six.text_type):
status = six.text_type(status, "utf-8")
except subprocess.CalledProcessError:
return False
current_dc = ''
for line in status.split('\n'):
if line.startswith('Current DC'):
# Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
current_dc = line.split(':')[1].split()[0]
if current_dc == get_unit_hostname():
return True
return False
@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound) @retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
def is_crm_leader(resource, retry=False): def is_crm_leader(resource, retry=False):
""" """
@ -104,6 +127,8 @@ def is_crm_leader(resource, retry=False):
We allow this operation to be retried to avoid the possibility of getting a We allow this operation to be retried to avoid the possibility of getting a
false negative. See LP #1396246 for more info. false negative. See LP #1396246 for more info.
""" """
if resource == DC_RESOURCE_NAME:
return is_crm_dc()
cmd = ['crm', 'resource', 'show', resource] cmd = ['crm', 'resource', 'show', resource]
try: try:
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)

View File

@ -46,15 +46,22 @@ class OpenStackAmuletDeployment(AmuletDeployment):
stable or next branches for the other_services.""" stable or next branches for the other_services."""
base_charms = ['mysql', 'mongodb'] base_charms = ['mysql', 'mongodb']
if self.series in ['precise', 'trusty']:
base_series = self.series
else:
base_series = self.current_next
if self.stable: if self.stable:
for svc in other_services: for svc in other_services:
temp = 'lp:charms/{}' temp = 'lp:charms/{}/{}'
svc['location'] = temp.format(svc['name']) svc['location'] = temp.format(base_series,
svc['name'])
else: else:
for svc in other_services: for svc in other_services:
if svc['name'] in base_charms: if svc['name'] in base_charms:
temp = 'lp:charms/{}' temp = 'lp:charms/{}/{}'
svc['location'] = temp.format(svc['name']) svc['location'] = temp.format(base_series,
svc['name'])
else: else:
temp = 'lp:~openstack-charmers/charms/{}/{}/next' temp = 'lp:~openstack-charmers/charms/{}/{}/next'
svc['location'] = temp.format(self.current_next, svc['location'] = temp.format(self.current_next,
@ -99,10 +106,12 @@ class OpenStackAmuletDeployment(AmuletDeployment):
Return an integer representing the enum value of the openstack Return an integer representing the enum value of the openstack
release. release.
""" """
# Must be ordered by OpenStack release (not by Ubuntu release):
(self.precise_essex, self.precise_folsom, self.precise_grizzly, (self.precise_essex, self.precise_folsom, self.precise_grizzly,
self.precise_havana, self.precise_icehouse, self.precise_havana, self.precise_icehouse,
self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
self.utopic_juno, self.vivid_kilo) = range(10) self.trusty_kilo, self.vivid_kilo) = range(10)
releases = { releases = {
('precise', None): self.precise_essex, ('precise', None): self.precise_essex,
('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-folsom'): self.precise_folsom,

View File

@ -17,6 +17,7 @@
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
config, config,
unit_get, unit_get,
service_name,
) )
from charmhelpers.contrib.network.ip import ( from charmhelpers.contrib.network.ip import (
get_address_in_network, get_address_in_network,
@ -82,6 +83,26 @@ def _get_scheme(configs):
return scheme return scheme
def _get_address_override(endpoint_type=PUBLIC):
"""Returns any address overrides that the user has defined based on the
endpoint type.
Note: this function allows for the service name to be inserted into the
address if the user specifies {service_name}.somehost.org.
:param endpoint_type: the type of endpoint to retrieve the override
value for.
:returns: any endpoint address or hostname that the user has overridden
or None if an override is not present.
"""
override_key = ADDRESS_MAP[endpoint_type]['override']
addr_override = config(override_key)
if not addr_override:
return None
else:
return addr_override.format(service_name=service_name())
def resolve_address(endpoint_type=PUBLIC): def resolve_address(endpoint_type=PUBLIC):
"""Return unit address depending on net config. """Return unit address depending on net config.
@ -93,14 +114,9 @@ def resolve_address(endpoint_type=PUBLIC):
:param endpoint_type: Network endpoing type :param endpoint_type: Network endpoing type
""" """
resolved_address = None resolved_address = _get_address_override(endpoint_type)
if resolved_address:
# Allow the user to override the address which is used. This is return resolved_address
# useful for proxy services or exposing a public endpoint url, etc.
override_key = ADDRESS_MAP[endpoint_type]['override']
addr_override = config(override_key)
if addr_override:
return addr_override
vips = config('vip') vips = config('vip')
if vips: if vips:

View File

@ -256,11 +256,14 @@ def network_manager():
def parse_mappings(mappings): def parse_mappings(mappings):
parsed = {} parsed = {}
if mappings: if mappings:
mappings = mappings.split(' ') mappings = mappings.split()
for m in mappings: for m in mappings:
p = m.partition(':') p = m.partition(':')
if p[1] == ':': key = p[0].strip()
parsed[p[0].strip()] = p[2].strip() if p[1]:
parsed[key] = p[2].strip()
else:
parsed[key] = ''
return parsed return parsed
@ -283,13 +286,13 @@ def parse_data_port_mappings(mappings, default_bridge='br-data'):
Returns dict of the form {bridge:port}. Returns dict of the form {bridge:port}.
""" """
_mappings = parse_mappings(mappings) _mappings = parse_mappings(mappings)
if not _mappings: if not _mappings or list(_mappings.values()) == ['']:
if not mappings: if not mappings:
return {} return {}
# For backwards-compatibility we need to support port-only provided in # For backwards-compatibility we need to support port-only provided in
# config. # config.
_mappings = {default_bridge: mappings.split(' ')[0]} _mappings = {default_bridge: mappings.split()[0]}
bridges = _mappings.keys() bridges = _mappings.keys()
ports = _mappings.values() ports = _mappings.values()
@ -309,6 +312,8 @@ def parse_vlan_range_mappings(mappings):
Mappings must be a space-delimited list of provider:start:end mappings. Mappings must be a space-delimited list of provider:start:end mappings.
The start:end range is optional and may be omitted.
Returns dict of the form {provider: (start, end)}. Returns dict of the form {provider: (start, end)}.
""" """
_mappings = parse_mappings(mappings) _mappings = parse_mappings(mappings)

View File

@ -53,9 +53,13 @@ from charmhelpers.contrib.network.ip import (
get_ipv6_addr get_ipv6_addr
) )
from charmhelpers.contrib.python.packages import (
pip_create_virtualenv,
pip_install,
)
from charmhelpers.core.host import lsb_release, mounts, umount from charmhelpers.core.host import lsb_release, mounts, umount
from charmhelpers.fetch import apt_install, apt_cache, install_remote from charmhelpers.fetch import apt_install, apt_cache, install_remote
from charmhelpers.contrib.python.packages import pip_install
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
@ -497,7 +501,17 @@ def git_install_requested():
requirements_dir = None requirements_dir = None
def git_clone_and_install(projects_yaml, core_project): def _git_yaml_load(projects_yaml):
"""
Load the specified yaml into a dictionary.
"""
if not projects_yaml:
return None
return yaml.load(projects_yaml)
def git_clone_and_install(projects_yaml, core_project, depth=1):
""" """
Clone/install all specified OpenStack repositories. Clone/install all specified OpenStack repositories.
@ -510,23 +524,22 @@ def git_clone_and_install(projects_yaml, core_project):
repository: 'git://git.openstack.org/openstack/requirements.git', repository: 'git://git.openstack.org/openstack/requirements.git',
branch: 'stable/icehouse'} branch: 'stable/icehouse'}
directory: /mnt/openstack-git directory: /mnt/openstack-git
http_proxy: http://squid.internal:3128 http_proxy: squid-proxy-url
https_proxy: https://squid.internal:3128 https_proxy: squid-proxy-url
The directory, http_proxy, and https_proxy keys are optional. The directory, http_proxy, and https_proxy keys are optional.
""" """
global requirements_dir global requirements_dir
parent_dir = '/mnt/openstack-git' parent_dir = '/mnt/openstack-git'
http_proxy = None
if not projects_yaml: projects = _git_yaml_load(projects_yaml)
return
projects = yaml.load(projects_yaml)
_git_validate_projects_yaml(projects, core_project) _git_validate_projects_yaml(projects, core_project)
old_environ = dict(os.environ) old_environ = dict(os.environ)
if 'http_proxy' in projects.keys(): if 'http_proxy' in projects.keys():
http_proxy = projects['http_proxy']
os.environ['http_proxy'] = projects['http_proxy'] os.environ['http_proxy'] = projects['http_proxy']
if 'https_proxy' in projects.keys(): if 'https_proxy' in projects.keys():
os.environ['https_proxy'] = projects['https_proxy'] os.environ['https_proxy'] = projects['https_proxy']
@ -534,15 +547,19 @@ def git_clone_and_install(projects_yaml, core_project):
if 'directory' in projects.keys(): if 'directory' in projects.keys():
parent_dir = projects['directory'] parent_dir = projects['directory']
pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
for p in projects['repositories']: for p in projects['repositories']:
repo = p['repository'] repo = p['repository']
branch = p['branch'] branch = p['branch']
if p['name'] == 'requirements': if p['name'] == 'requirements':
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, repo_dir = _git_clone_and_install_single(repo, branch, depth,
parent_dir, http_proxy,
update_requirements=False) update_requirements=False)
requirements_dir = repo_dir requirements_dir = repo_dir
else: else:
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, repo_dir = _git_clone_and_install_single(repo, branch, depth,
parent_dir, http_proxy,
update_requirements=True) update_requirements=True)
os.environ = old_environ os.environ = old_environ
@ -574,7 +591,8 @@ def _git_ensure_key_exists(key, keys):
error_out('openstack-origin-git key \'{}\' is missing'.format(key)) error_out('openstack-origin-git key \'{}\' is missing'.format(key))
def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements): def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
update_requirements):
""" """
Clone and install a single git repository. Clone and install a single git repository.
""" """
@ -587,7 +605,8 @@ def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements)
if not os.path.exists(dest_dir): if not os.path.exists(dest_dir):
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
repo_dir = install_remote(repo, dest=parent_dir, branch=branch) repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
depth=depth)
else: else:
repo_dir = dest_dir repo_dir = dest_dir
@ -598,7 +617,12 @@ def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements)
_git_update_requirements(repo_dir, requirements_dir) _git_update_requirements(repo_dir, requirements_dir)
juju_log('Installing git repo from dir: {}'.format(repo_dir)) juju_log('Installing git repo from dir: {}'.format(repo_dir))
pip_install(repo_dir) if http_proxy:
pip_install(repo_dir, proxy=http_proxy,
venv=os.path.join(parent_dir, 'venv'))
else:
pip_install(repo_dir,
venv=os.path.join(parent_dir, 'venv'))
return repo_dir return repo_dir
@ -621,16 +645,27 @@ def _git_update_requirements(package_dir, reqs_dir):
os.chdir(orig_dir) os.chdir(orig_dir)
def git_pip_venv_dir(projects_yaml):
"""
Return the pip virtualenv path.
"""
parent_dir = '/mnt/openstack-git'
projects = _git_yaml_load(projects_yaml)
if 'directory' in projects.keys():
parent_dir = projects['directory']
return os.path.join(parent_dir, 'venv')
def git_src_dir(projects_yaml, project): def git_src_dir(projects_yaml, project):
""" """
Return the directory where the specified project's source is located. Return the directory where the specified project's source is located.
""" """
parent_dir = '/mnt/openstack-git' parent_dir = '/mnt/openstack-git'
if not projects_yaml: projects = _git_yaml_load(projects_yaml)
return
projects = yaml.load(projects_yaml)
if 'directory' in projects.keys(): if 'directory' in projects.keys():
parent_dir = projects['directory'] parent_dir = projects['directory']
@ -640,3 +675,15 @@ def git_src_dir(projects_yaml, project):
return os.path.join(parent_dir, os.path.basename(p['repository'])) return os.path.join(parent_dir, os.path.basename(p['repository']))
return None return None
def git_yaml_value(projects_yaml, key):
"""
Return the value in projects_yaml for the specified key.
"""
projects = _git_yaml_load(projects_yaml)
if key in projects.keys():
return projects[key]
return None

View File

@ -17,8 +17,11 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import os
import subprocess
from charmhelpers.fetch import apt_install, apt_update from charmhelpers.fetch import apt_install, apt_update
from charmhelpers.core.hookenv import log from charmhelpers.core.hookenv import charm_dir, log
try: try:
from pip import main as pip_execute from pip import main as pip_execute
@ -51,11 +54,15 @@ def pip_install_requirements(requirements, **options):
pip_execute(command) pip_execute(command)
def pip_install(package, fatal=False, upgrade=False, **options): def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
"""Install a python package""" """Install a python package"""
command = ["install"] if venv:
venv_python = os.path.join(venv, 'bin/pip')
command = [venv_python, "install"]
else:
command = ["install"]
available_options = ('proxy', 'src', 'log', "index-url", ) available_options = ('proxy', 'src', 'log', 'index-url', )
for option in parse_options(options, available_options): for option in parse_options(options, available_options):
command.append(option) command.append(option)
@ -69,7 +76,10 @@ def pip_install(package, fatal=False, upgrade=False, **options):
log("Installing {} package with options: {}".format(package, log("Installing {} package with options: {}".format(package,
command)) command))
pip_execute(command) if venv:
subprocess.check_call(command)
else:
pip_execute(command)
def pip_uninstall(package, **options): def pip_uninstall(package, **options):
@ -94,3 +104,16 @@ def pip_list():
"""Returns the list of current python installed packages """Returns the list of current python installed packages
""" """
return pip_execute(["list"]) return pip_execute(["list"])
def pip_create_virtualenv(path=None):
"""Create an isolated Python environment."""
apt_install('python-virtualenv')
if path:
venv_path = path
else:
venv_path = os.path.join(charm_dir(), 'venv')
if not os.path.exists(venv_path):
subprocess.check_call(['virtualenv', venv_path])

View File

@ -21,12 +21,14 @@
# Charm Helpers Developers <juju@lists.ubuntu.com> # Charm Helpers Developers <juju@lists.ubuntu.com>
from __future__ import print_function from __future__ import print_function
from functools import wraps
import os import os
import json import json
import yaml import yaml
import subprocess import subprocess
import sys import sys
import errno import errno
import tempfile
from subprocess import CalledProcessError from subprocess import CalledProcessError
import six import six
@ -58,15 +60,17 @@ def cached(func):
will cache the result of unit_get + 'test' for future calls. will cache the result of unit_get + 'test' for future calls.
""" """
@wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
global cache global cache
key = str((func, args, kwargs)) key = str((func, args, kwargs))
try: try:
return cache[key] return cache[key]
except KeyError: except KeyError:
res = func(*args, **kwargs) pass # Drop out of the exception handler scope.
cache[key] = res res = func(*args, **kwargs)
return res cache[key] = res
return res
return wrapper return wrapper
@ -178,7 +182,7 @@ def local_unit():
def remote_unit(): def remote_unit():
"""The remote unit for the current relation hook""" """The remote unit for the current relation hook"""
return os.environ['JUJU_REMOTE_UNIT'] return os.environ.get('JUJU_REMOTE_UNIT', None)
def service_name(): def service_name():
@ -250,6 +254,12 @@ class Config(dict):
except KeyError: except KeyError:
return (self._prev_dict or {})[key] return (self._prev_dict or {})[key]
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def keys(self): def keys(self):
prev_keys = [] prev_keys = []
if self._prev_dict is not None: if self._prev_dict is not None:
@ -353,18 +363,49 @@ def relation_set(relation_id=None, relation_settings=None, **kwargs):
"""Set relation information for the current unit""" """Set relation information for the current unit"""
relation_settings = relation_settings if relation_settings else {} relation_settings = relation_settings if relation_settings else {}
relation_cmd_line = ['relation-set'] relation_cmd_line = ['relation-set']
accepts_file = "--file" in subprocess.check_output(
relation_cmd_line + ["--help"], universal_newlines=True)
if relation_id is not None: if relation_id is not None:
relation_cmd_line.extend(('-r', relation_id)) relation_cmd_line.extend(('-r', relation_id))
for k, v in (list(relation_settings.items()) + list(kwargs.items())): settings = relation_settings.copy()
if v is None: settings.update(kwargs)
relation_cmd_line.append('{}='.format(k)) for key, value in settings.items():
else: # Force value to be a string: it always should, but some call
relation_cmd_line.append('{}={}'.format(k, v)) # sites pass in things like dicts or numbers.
subprocess.check_call(relation_cmd_line) if value is not None:
settings[key] = "{}".format(value)
if accepts_file:
# --file was introduced in Juju 1.23.2. Use it by default if
# available, since otherwise we'll break if the relation data is
# too big. Ideally we should tell relation-set to read the data from
# stdin, but that feature is broken in 1.23.2: Bug #1454678.
with tempfile.NamedTemporaryFile(delete=False) as settings_file:
settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
subprocess.check_call(
relation_cmd_line + ["--file", settings_file.name])
os.remove(settings_file.name)
else:
for key, value in settings.items():
if value is None:
relation_cmd_line.append('{}='.format(key))
else:
relation_cmd_line.append('{}={}'.format(key, value))
subprocess.check_call(relation_cmd_line)
# Flush cache of any relation-gets for local unit # Flush cache of any relation-gets for local unit
flush(local_unit()) flush(local_unit())
def relation_clear(r_id=None):
''' Clears any relation data already set on relation r_id '''
settings = relation_get(rid=r_id,
unit=local_unit())
for setting in settings:
if setting not in ['public-address', 'private-address']:
settings[setting] = None
relation_set(relation_id=r_id,
**settings)
@cached @cached
def relation_ids(reltype=None): def relation_ids(reltype=None):
"""A list of relation_ids""" """A list of relation_ids"""
@ -509,6 +550,11 @@ def unit_get(attribute):
return None return None
def unit_public_ip():
"""Get this unit's public IP address"""
return unit_get('public-address')
def unit_private_ip(): def unit_private_ip():
"""Get this unit's private IP address""" """Get this unit's private IP address"""
return unit_get('private-address') return unit_get('private-address')
@ -605,3 +651,94 @@ def action_fail(message):
The results set by action_set are preserved.""" The results set by action_set are preserved."""
subprocess.check_call(['action-fail', message]) subprocess.check_call(['action-fail', message])
def status_set(workload_state, message):
"""Set the workload state with a message
Use status-set to set the workload state with a message which is visible
to the user via juju status. If the status-set command is not found then
assume this is juju < 1.23 and juju-log the message unstead.
workload_state -- valid juju workload state.
message -- status update message
"""
valid_states = ['maintenance', 'blocked', 'waiting', 'active']
if workload_state not in valid_states:
raise ValueError(
'{!r} is not a valid workload state'.format(workload_state)
)
cmd = ['status-set', workload_state, message]
try:
ret = subprocess.call(cmd)
if ret == 0:
return
except OSError as e:
if e.errno != errno.ENOENT:
raise
log_message = 'status-set failed: {} {}'.format(workload_state,
message)
log(log_message, level='INFO')
def status_get():
"""Retrieve the previously set juju workload state
If the status-set command is not found then assume this is juju < 1.23 and
return 'unknown'
"""
cmd = ['status-get']
try:
raw_status = subprocess.check_output(cmd, universal_newlines=True)
status = raw_status.rstrip()
return status
except OSError as e:
if e.errno == errno.ENOENT:
return 'unknown'
else:
raise
def translate_exc(from_exc, to_exc):
def inner_translate_exc1(f):
def inner_translate_exc2(*args, **kwargs):
try:
return f(*args, **kwargs)
except from_exc:
raise to_exc
return inner_translate_exc2
return inner_translate_exc1
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def is_leader():
"""Does the current unit hold the juju leadership
Uses juju to determine whether the current unit is the leader of its peers
"""
cmd = ['is-leader', '--format=json']
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def leader_get(attribute=None):
"""Juju leader get value(s)"""
cmd = ['leader-get', '--format=json'] + [attribute or '-']
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def leader_set(settings=None, **kwargs):
"""Juju leader set value(s)"""
log("Juju leader-set '%s'" % (settings), level=DEBUG)
cmd = ['leader-set']
settings = settings or {}
settings.update(kwargs)
for k, v in settings.iteritems():
if v is None:
cmd.append('{}='.format(k))
else:
cmd.append('{}={}'.format(k, v))
subprocess.check_call(cmd)

View File

@ -90,7 +90,7 @@ def service_available(service_name):
['service', service_name, 'status'], ['service', service_name, 'status'],
stderr=subprocess.STDOUT).decode('UTF-8') stderr=subprocess.STDOUT).decode('UTF-8')
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
return 'unrecognized service' not in e.output return b'unrecognized service' not in e.output
else: else:
return True return True

View File

@ -15,9 +15,9 @@
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import os import os
import re
import json import json
from collections import Iterable from inspect import getargspec
from collections import Iterable, OrderedDict
from charmhelpers.core import host from charmhelpers.core import host
from charmhelpers.core import hookenv from charmhelpers.core import hookenv
@ -119,7 +119,7 @@ class ServiceManager(object):
""" """
self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
self._ready = None self._ready = None
self.services = {} self.services = OrderedDict()
for service in services or []: for service in services or []:
service_name = service['service'] service_name = service['service']
self.services[service_name] = service self.services[service_name] = service
@ -132,8 +132,8 @@ class ServiceManager(object):
if hook_name == 'stop': if hook_name == 'stop':
self.stop_services() self.stop_services()
else: else:
self.provide_data()
self.reconfigure_services() self.reconfigure_services()
self.provide_data()
cfg = hookenv.config() cfg = hookenv.config()
if cfg.implicit_save: if cfg.implicit_save:
cfg.save() cfg.save()
@ -145,15 +145,36 @@ class ServiceManager(object):
A provider must have a `name` attribute, which indicates which relation A provider must have a `name` attribute, which indicates which relation
to set data on, and a `provide_data()` method, which returns a dict of to set data on, and a `provide_data()` method, which returns a dict of
data to set. data to set.
The `provide_data()` method can optionally accept two parameters:
* ``remote_service`` The name of the remote service that the data will
be provided to. The `provide_data()` method will be called once
for each connected service (not unit). This allows the method to
tailor its data to the given service.
* ``service_ready`` Whether or not the service definition had all of
its requirements met, and thus the ``data_ready`` callbacks run.
Note that the ``provided_data`` methods are now called **after** the
``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
a chance to generate any data necessary for the providing to the remote
services.
""" """
hook_name = hookenv.hook_name() for service_name, service in self.services.items():
for service in self.services.values(): service_ready = self.is_ready(service_name)
for provider in service.get('provided_data', []): for provider in service.get('provided_data', []):
if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name): for relid in hookenv.relation_ids(provider.name):
data = provider.provide_data() units = hookenv.related_units(relid)
_ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data if not units:
if _ready: continue
hookenv.relation_set(None, data) remote_service = units[0].split('/')[0]
argspec = getargspec(provider.provide_data)
if len(argspec.args) > 1:
data = provider.provide_data(remote_service, service_ready)
else:
data = provider.provide_data()
if data:
hookenv.relation_set(relid, data)
def reconfigure_services(self, *service_names): def reconfigure_services(self, *service_names):
""" """

View File

@ -158,7 +158,7 @@ def filter_installed_packages(packages):
def apt_cache(in_memory=True): def apt_cache(in_memory=True):
"""Build and return an apt cache""" """Build and return an apt cache"""
import apt_pkg from apt import apt_pkg
apt_pkg.init() apt_pkg.init()
if in_memory: if in_memory:
apt_pkg.config.set("Dir::Cache::pkgcache", "") apt_pkg.config.set("Dir::Cache::pkgcache", "")

View File

@ -45,14 +45,16 @@ class GitUrlFetchHandler(BaseFetchHandler):
else: else:
return True return True
def clone(self, source, dest, branch): def clone(self, source, dest, branch, depth=None):
if not self.can_handle(source): if not self.can_handle(source):
raise UnhandledSource("Cannot handle {}".format(source)) raise UnhandledSource("Cannot handle {}".format(source))
repo = Repo.clone_from(source, dest) if depth:
repo.git.checkout(branch) Repo.clone_from(source, dest, branch=branch, depth=depth)
else:
Repo.clone_from(source, dest, branch=branch)
def install(self, source, branch="master", dest=None): def install(self, source, branch="master", dest=None, depth=None):
url_parts = self.parse_url(source) url_parts = self.parse_url(source)
branch_name = url_parts.path.strip("/").split("/")[-1] branch_name = url_parts.path.strip("/").split("/")[-1]
if dest: if dest:
@ -63,7 +65,7 @@ class GitUrlFetchHandler(BaseFetchHandler):
if not os.path.exists(dest_dir): if not os.path.exists(dest_dir):
mkdir(dest_dir, perms=0o755) mkdir(dest_dir, perms=0o755)
try: try:
self.clone(source, dest_dir, branch) self.clone(source, dest_dir, branch, depth)
except GitCommandError as e: except GitCommandError as e:
raise UnhandledSource(e.message) raise UnhandledSource(e.message)
except OSError as e: except OSError as e:

View File

@ -79,6 +79,9 @@ class AmuletUtils(object):
for k, v in six.iteritems(commands): for k, v in six.iteritems(commands):
for cmd in v: for cmd in v:
output, code = k.run(cmd) output, code = k.run(cmd)
self.log.debug('{} `{}` returned '
'{}'.format(k.info['unit_name'],
cmd, code))
if code != 0: if code != 0:
return "command `{}` returned {}".format(cmd, str(code)) return "command `{}` returned {}".format(cmd, str(code))
return None return None
@ -86,7 +89,11 @@ class AmuletUtils(object):
def _get_config(self, unit, filename): def _get_config(self, unit, filename):
"""Get a ConfigParser object for parsing a unit's config file.""" """Get a ConfigParser object for parsing a unit's config file."""
file_contents = unit.file_contents(filename) file_contents = unit.file_contents(filename)
config = ConfigParser.ConfigParser()
# NOTE(beisner): by default, ConfigParser does not handle options
# with no value, such as the flags used in the mysql my.cnf file.
# https://bugs.python.org/issue7005
config = ConfigParser.ConfigParser(allow_no_value=True)
config.readfp(io.StringIO(file_contents)) config.readfp(io.StringIO(file_contents))
return config return config

View File

@ -46,15 +46,22 @@ class OpenStackAmuletDeployment(AmuletDeployment):
stable or next branches for the other_services.""" stable or next branches for the other_services."""
base_charms = ['mysql', 'mongodb'] base_charms = ['mysql', 'mongodb']
if self.series in ['precise', 'trusty']:
base_series = self.series
else:
base_series = self.current_next
if self.stable: if self.stable:
for svc in other_services: for svc in other_services:
temp = 'lp:charms/{}' temp = 'lp:charms/{}/{}'
svc['location'] = temp.format(svc['name']) svc['location'] = temp.format(base_series,
svc['name'])
else: else:
for svc in other_services: for svc in other_services:
if svc['name'] in base_charms: if svc['name'] in base_charms:
temp = 'lp:charms/{}' temp = 'lp:charms/{}/{}'
svc['location'] = temp.format(svc['name']) svc['location'] = temp.format(base_series,
svc['name'])
else: else:
temp = 'lp:~openstack-charmers/charms/{}/{}/next' temp = 'lp:~openstack-charmers/charms/{}/{}/next'
svc['location'] = temp.format(self.current_next, svc['location'] = temp.format(self.current_next,
@ -99,10 +106,12 @@ class OpenStackAmuletDeployment(AmuletDeployment):
Return an integer representing the enum value of the openstack Return an integer representing the enum value of the openstack
release. release.
""" """
# Must be ordered by OpenStack release (not by Ubuntu release):
(self.precise_essex, self.precise_folsom, self.precise_grizzly, (self.precise_essex, self.precise_folsom, self.precise_grizzly,
self.precise_havana, self.precise_icehouse, self.precise_havana, self.precise_icehouse,
self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
self.utopic_juno, self.vivid_kilo) = range(10) self.trusty_kilo, self.vivid_kilo) = range(10)
releases = { releases = {
('precise', None): self.precise_essex, ('precise', None): self.precise_essex,
('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-folsom'): self.precise_folsom,

View File

@ -323,12 +323,16 @@ class CephRadosGWTests(CharmTestCase):
cmd = ['service', 'radosgw', 'restart'] cmd = ['service', 'radosgw', 'restart']
self.subprocess.call.assert_called_with(cmd) self.subprocess.call.assert_called_with(cmd)
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.config') @patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined_early_version(self, _config): def test_identity_joined_early_version(self, _config):
self.cmp_pkgrevno.return_value = -1 self.cmp_pkgrevno.return_value = -1
ceph_hooks.identity_joined() ceph_hooks.identity_joined()
self.sys.exit.assert_called_with(1) self.sys.exit.assert_called_with(1)
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.resolve_address') @patch('charmhelpers.contrib.openstack.ip.resolve_address')
@patch('charmhelpers.contrib.openstack.ip.config') @patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined(self, _config, _resolve_address): def test_identity_joined(self, _config, _resolve_address):
@ -348,6 +352,8 @@ class CephRadosGWTests(CharmTestCase):
relation_id='rid', relation_id='rid',
admin_url='http://myserv:80/swift') admin_url='http://myserv:80/swift')
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.is_clustered') @patch('charmhelpers.contrib.openstack.ip.is_clustered')
@patch('charmhelpers.contrib.openstack.ip.unit_get') @patch('charmhelpers.contrib.openstack.ip.unit_get')
@patch('charmhelpers.contrib.openstack.ip.config') @patch('charmhelpers.contrib.openstack.ip.config')