yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00680
[Merge] lp:~yellow/charm-tools/trunk into lp:charm-tools
Brad Crittenden has proposed merging lp:~yellow/charm-tools/trunk into lp:charm-tools.
Requested reviews:
charmers (charmers)
For more details, see:
https://code.launchpad.net/~yellow/charm-tools/trunk/+merge/101554
Update parsing of 'juju status' output to account for change in tokens ('state' -> 'agent-state').
--
https://code.launchpad.net/~yellow/charm-tools/trunk/+merge/101554
Your team Launchpad Yellow Squad is subscribed to branch lp:~yellow/charm-tools/trunk.
=== modified file 'Makefile'
--- Makefile 2012-01-03 18:19:56 +0000
+++ Makefile 2012-04-11 13:26:47 +0000
@@ -25,3 +25,4 @@
tests/helpers/helpers.sh || sh -x tests/helpers/helpers.sh timeout
@echo Test shell helpers with bash
bash tests/helpers/helpers.sh || bash -x tests/helpers/helpers.sh timeout
+ python helpers/python/charmhelpers/tests/test_charmhelpers.py
=== added file 'ez_setup.py'
--- ez_setup.py 1970-01-01 00:00:00 +0000
+++ ez_setup.py 2012-04-11 13:26:47 +0000
@@ -0,0 +1,288 @@
+#!python
+
+# NOTE TO LAUNCHPAD DEVELOPERS: This is a bootstrapping file from the
+# setuptools project. It is imported by our setup.py.
+
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c11"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090',
+ 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4',
+ 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7',
+ 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5',
+ 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de',
+ 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b',
+ 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2',
+ 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
=== added directory 'helpers/python'
=== added directory 'helpers/python/charmhelpers'
=== added file 'helpers/python/charmhelpers/__init__.py'
--- helpers/python/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ helpers/python/charmhelpers/__init__.py 2012-04-11 13:26:47 +0000
@@ -0,0 +1,185 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Helper functions for writing Juju charms in Python."""
+
+__metaclass__ = type
+__all__ = [
+ 'get_config',
+ 'log',
+ 'log_entry',
+ 'log_exit',
+ 'relation_get',
+ 'relation_set',
+ 'unit_info',
+ 'wait_for_machine',
+ 'wait_for_page_contents',
+ 'wait_for_relation',
+ 'wait_for_unit',
+ ]
+
+from collections import namedtuple
+import json
+import operator
+from shelltoolbox import (
+ command,
+ script_name,
+ )
+import tempfile
+import time
+import urllib2
+import yaml
+
+
+SLEEP_AMOUNT = 0.1
+Env = namedtuple('Env', 'uid gid home')
+log = command('juju-log')
+# We create a juju_status Command here because it makes testing much,
+# much easier.
+juju_status = lambda: command('juju')('status')
+
+
+def log_entry():
+ log("--> Entering {}".format(script_name()))
+
+
+def log_exit():
+ log("<-- Exiting {}".format(script_name()))
+
+
+def get_config():
+ config_get = command('config-get', '--format=json')
+ return json.loads(config_get())
+
+
+def relation_get(*args):
+ cmd = command('relation-get')
+ return cmd(*args).strip()
+
+
+def relation_set(**kwargs):
+ cmd = command('relation-set')
+ args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
+ return cmd(*args)
+
+
+def make_charm_config_file(charm_config):
+ charm_config_file = tempfile.NamedTemporaryFile()
+ charm_config_file.write(yaml.dump(charm_config))
+ charm_config_file.flush()
+ # The NamedTemporaryFile instance is returned instead of just the name
+ # because we want to take advantage of garbage collection-triggered
+ # deletion of the temp file when it goes out of scope in the caller.
+ return charm_config_file
+
+
+def unit_info(service_name, item_name, data=None, unit=None):
+ if data is None:
+ data = yaml.safe_load(juju_status())
+ service = data['services'].get(service_name)
+ if service is None:
+ # XXX 2012-02-08 gmb:
+ # This allows us to cope with the race condition that we
+ # have between deploying a service and having it come up in
+ # `juju status`. We could probably do with cleaning it up so
+ # that it fails a bit more noisily after a while.
+ return ''
+ units = service['units']
+ if unit is not None:
+ item = units[unit][item_name]
+ else:
+ # It might seem odd to sort the units here, but we do it to
+ # ensure that when no unit is specified, the first unit for the
+ # service (or at least the one with the lowest number) is the
+ # one whose data gets returned.
+ sorted_unit_names = sorted(units.keys())
+ item = units[sorted_unit_names[0]][item_name]
+ return item
+
+
+def get_machine_data():
+ return yaml.safe_load(juju_status())['machines']
+
+
+def wait_for_machine(num_machines=1, timeout=300):
+ """Wait `timeout` seconds for `num_machines` machines to come up.
+
+ This wait_for... function can be called by other wait_for functions
+ whose timeouts might be too short in situations where only a bare
+ Juju setup has been bootstrapped.
+
+ :return: A tuple of (num_machines, time_taken). This is used for
+ testing.
+ """
+ # You may think this is a hack, and you'd be right. The easiest way
+ # to tell what environment we're working in (LXC vs EC2) is to check
+ # the dns-name of the first machine. If it's localhost we're in LXC
+ # and we can just return here.
+ if get_machine_data()[0]['dns-name'] == 'localhost':
+ return 1, 0
+ start_time = time.time()
+ while True:
+ # Drop the first machine, since it's the Zookeeper and that's
+ # not a machine that we need to wait for. This will only work
+ # for EC2 environments, which is why we return early above if
+ # we're in LXC.
+ machine_data = get_machine_data()
+ non_zookeeper_machines = [
+ machine_data[key] for key in machine_data.keys()[1:]]
+ if len(non_zookeeper_machines) >= num_machines:
+ all_machines_running = True
+ for machine in non_zookeeper_machines:
+ if machine.get('instance-state') != 'running':
+ all_machines_running = False
+ break
+ if all_machines_running:
+ break
+ if time.time() - start_time >= timeout:
+ raise RuntimeError('timeout waiting for service to start')
+ time.sleep(SLEEP_AMOUNT)
+ return num_machines, time.time() - start_time
+
+
+def wait_for_unit(service_name, timeout=480):
+ """Wait `timeout` seconds for a given service name to come up."""
+ wait_for_machine(num_machines=1)
+ start_time = time.time()
+ while True:
+ state = unit_info(service_name, 'agent-state')
+ if 'error' in state or state == 'started':
+ break
+ if time.time() - start_time >= timeout:
+ raise RuntimeError('timeout waiting for service to start')
+ time.sleep(SLEEP_AMOUNT)
+ if state != 'started':
+ raise RuntimeError('unit did not start, agent-state: ' + state)
+
+
+def wait_for_relation(service_name, relation_name, timeout=120):
+ """Wait `timeout` seconds for a given relation to come up."""
+ start_time = time.time()
+ while True:
+ relation = unit_info(service_name, 'relations').get(relation_name)
+ if relation is not None and relation['state'] == 'up':
+ break
+ if time.time() - start_time >= timeout:
+ raise RuntimeError('timeout waiting for relation to be up')
+ time.sleep(SLEEP_AMOUNT)
+
+
+def wait_for_page_contents(url, contents, timeout=120, validate=None):
+ if validate is None:
+ validate = operator.contains
+ start_time = time.time()
+ while True:
+ try:
+ stream = urllib2.urlopen(url)
+ except (urllib2.HTTPError, urllib2.URLError):
+ pass
+ else:
+ page = stream.read()
+ if validate(page, contents):
+ return page
+ if time.time() - start_time >= timeout:
+ raise RuntimeError('timeout waiting for contents of ' + url)
+ time.sleep(SLEEP_AMOUNT)
=== added directory 'helpers/python/charmhelpers/tests'
=== added file 'helpers/python/charmhelpers/tests/test_charmhelpers.py'
--- helpers/python/charmhelpers/tests/test_charmhelpers.py 1970-01-01 00:00:00 +0000
+++ helpers/python/charmhelpers/tests/test_charmhelpers.py 2012-04-11 13:26:47 +0000
@@ -0,0 +1,337 @@
+# Tests for Python charm helpers.
+
+import unittest
+import yaml
+
+from simplejson import dumps
+from StringIO import StringIO
+from testtools import TestCase
+
+import sys
+# Path hack to ensure we test the local code, not a version installed in
+# /usr/local/lib. This is necessary since /usr/local/lib is prepended before
+# what is specified in PYTHONPATH.
+sys.path.insert(0, 'helpers/python')
+import charmhelpers
+
+
+class CharmHelpersTestCase(TestCase):
+ """A basic test case for Python charm helpers."""
+
+ def _patch_command(self, replacement_command):
+ """Monkeypatch charmhelpers.command for testing purposes.
+
+ :param replacement_command: The replacement Callable for
+ command().
+ """
+ new_command = lambda *args: replacement_command
+ self.patch(charmhelpers, 'command', new_command)
+
+ def _make_juju_status_dict(self, num_units=1,
+ service_name='test-service',
+ unit_state='pending',
+ machine_state='not-started'):
+ """Generate valid juju status dict and return it."""
+ machine_data = {}
+ # The 0th machine is the Zookeeper.
+ machine_data[0] = {
+ 'dns-name': 'zookeeper.example.com',
+ 'instance-id': 'machine0',
+ 'state': 'not-started',
+ }
+ service_data = {
+ 'charm': 'local:precise/{}-1'.format(service_name),
+ 'relations': {},
+ 'units': {},
+ }
+ for i in range(num_units):
+ # The machine is always going to be i+1 because there
+ # will always be num_units+1 machines.
+ machine_number = i+1
+ unit_machine_data = {
+ 'dns-name': 'machine{}.example.com'.format(machine_number),
+ 'instance-id': 'machine{}'.format(machine_number),
+ 'state': machine_state,
+ 'instance-state': machine_state,
+ }
+ machine_data[machine_number] = unit_machine_data
+ unit_data = {
+ 'machine': machine_number,
+ 'public-address':
+ '{}-{}.example.com'.format(service_name, i),
+ 'relations': {
+ 'db': {'state': 'up'},
+ },
+ 'agent-state': unit_state,
+ }
+ service_data['units']['{}/{}'.format(service_name, i)] = (
+ unit_data)
+ juju_status_data = {
+ 'machines': machine_data,
+ 'services': {service_name: service_data},
+ }
+ return juju_status_data
+
+ def _make_juju_status_yaml(self, num_units=1,
+ service_name='test-service',
+ unit_state='pending',
+ machine_state='not-started'):
+ """Convert the dict returned by `_make_juju_status_dict` to YAML."""
+ return yaml.dump(
+ self._make_juju_status_dict(
+ num_units, service_name, unit_state, machine_state))
+
+ def test_get_config(self):
+ # get_config returns the contents of the current charm
+ # configuration, as returned by config-get --format=json.
+ mock_config = {'key': 'value'}
+
+ # Monkey-patch shelltoolbox.command to avoid having to call out
+ # to config-get.
+ self._patch_command(lambda: dumps(mock_config))
+ self.assertEqual(mock_config, charmhelpers.get_config())
+
+ def test_relation_get(self):
+ # relation_get returns the value of a given relation variable,
+ # as returned by relation-get $VAR.
+ mock_relation_values = {
+ 'foo': 'bar',
+ 'spam': 'eggs',
+ }
+ self._patch_command(lambda *args: mock_relation_values[args[0]])
+ self.assertEqual('bar', charmhelpers.relation_get('foo'))
+ self.assertEqual('eggs', charmhelpers.relation_get('spam'))
+
+ def test_relation_set(self):
+ # relation_set calls out to relation-set and passes key=value
+ # pairs to it.
+ items_set = {}
+ def mock_relation_set(*args):
+ for arg in args:
+ key, value = arg.split("=")
+ items_set[key] = value
+ self._patch_command(mock_relation_set)
+ charmhelpers.relation_set(foo='bar', spam='eggs')
+ self.assertEqual('bar', items_set.get('foo'))
+ self.assertEqual('eggs', items_set.get('spam'))
+
+ def test_make_charm_config_file(self):
+ # make_charm_config_file() writes the passed configuration to a
+ # temporary file as YAML.
+ charm_config = {
+ 'foo': 'bar',
+ 'spam': 'eggs',
+ 'ham': 'jam',
+ }
+ # make_charm_config_file() returns the file object so that it
+ # can be garbage collected properly.
+ charm_config_file = charmhelpers.make_charm_config_file(charm_config)
+ with open(charm_config_file.name) as config_in:
+ written_config = config_in.read()
+ self.assertEqual(yaml.dump(charm_config), written_config)
+
+ def test_unit_info(self):
+ # unit_info returns requested data about a given service.
+ juju_yaml = self._make_juju_status_yaml()
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertEqual(
+ 'pending',
+ charmhelpers.unit_info('test-service', 'agent-state'))
+
+ def test_unit_info_returns_empty_for_nonexistent_service(self):
+ # If the service passed to unit_info() has not yet started (or
+ # otherwise doesn't exist), unit_info() will return an empty
+ # string.
+ juju_yaml = "services: {}"
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertEqual(
+ '', charmhelpers.unit_info('test-service', 'state'))
+
+ def test_unit_info_accepts_data(self):
+ # It's possible to pass a `data` dict, containing the parsed
+ # result of juju status, to unit_info().
+ juju_status_data = yaml.safe_load(
+ self._make_juju_status_yaml())
+ self.patch(charmhelpers, 'juju_status', lambda: None)
+ service_data = juju_status_data['services']['test-service']
+ unit_info_dict = service_data['units']['test-service/0']
+ for key, value in unit_info_dict.items():
+ item_info = charmhelpers.unit_info(
+ 'test-service', key, data=juju_status_data)
+ self.assertEqual(value, item_info)
+
+ def test_unit_info_returns_first_unit_by_default(self):
+ # By default, unit_info() just returns the value of the
+ # requested item for the first unit in a service.
+ juju_yaml = self._make_juju_status_yaml(num_units=2)
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ unit_address = charmhelpers.unit_info(
+ 'test-service', 'public-address')
+ self.assertEqual('test-service-0.example.com', unit_address)
+
+ def test_unit_info_accepts_unit_name(self):
+ # By default, unit_info() just returns the value of the
+ # requested item for the first unit in a service. However, it's
+ # possible to pass a unit name to it, too.
+ juju_yaml = self._make_juju_status_yaml(num_units=2)
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ unit_address = charmhelpers.unit_info(
+ 'test-service', 'public-address', unit='test-service/1')
+ self.assertEqual('test-service-1.example.com', unit_address)
+
+ def test_get_machine_data(self):
+ # get_machine_data() returns a dict containing the machine data
+ # parsed from juju status.
+ juju_yaml = self._make_juju_status_yaml()
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ machine_0_data = charmhelpers.get_machine_data()[0]
+ self.assertEqual('zookeeper.example.com', machine_0_data['dns-name'])
+
+ def test_wait_for_machine_returns_if_machine_up(self):
+ # If wait_for_machine() is called and the machine(s) it is
+ # waiting for are already up, it will return.
+ juju_yaml = self._make_juju_status_yaml(machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ machines, time_taken = charmhelpers.wait_for_machine(timeout=1)
+ self.assertEqual(1, machines)
+
+ def test_wait_for_machine_times_out(self):
+ # If the machine that wait_for_machine is waiting for isn't
+ # 'running' before the passed timeout is reached,
+ # wait_for_machine will raise an error.
+ juju_yaml = self._make_juju_status_yaml()
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_machine, timeout=0)
+
+ def test_wait_for_machine_always_returns_if_running_locally(self):
+ # If juju is actually running against a local LXC container,
+ # wait_for_machine will always return.
+ juju_status_dict = self._make_juju_status_dict()
+ # We'll update the 0th machine to make it look like it's an LXC
+ # container.
+ juju_status_dict['machines'][0]['dns-name'] = 'localhost'
+ juju_yaml = yaml.dump(juju_status_dict)
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ machines, time_taken = charmhelpers.wait_for_machine(timeout=1)
+ # wait_for_machine will always return 1 machine started here,
+ # since there's only one machine to start.
+ self.assertEqual(1, machines)
+ # time_taken will be 0, since no actual waiting happened.
+ self.assertEqual(0, time_taken)
+
+ def test_wait_for_machine_waits_for_multiple_machines(self):
+ # wait_for_machine can be told to wait for multiple machines.
+ juju_yaml = self._make_juju_status_yaml(
+ num_units=2, machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ machines, time_taken = charmhelpers.wait_for_machine(num_machines=2)
+ self.assertEqual(2, machines)
+
+ def test_wait_for_unit_returns_if_unit_started(self):
+ # wait_for_unit() will return if the service it's waiting for is
+ # already up.
+ juju_yaml = self._make_juju_status_yaml(
+ unit_state='started', machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ charmhelpers.wait_for_unit('test-service', timeout=0)
+
+ def test_wait_for_unit_raises_error_on_error_state(self):
+ # If the unit is in some kind of error state, wait_for_unit will
+ # raise a RuntimeError.
+ juju_yaml = self._make_juju_status_yaml(
+ unit_state='start-error', machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_unit, 'test-service', timeout=0)
+
+ def test_wait_for_unit_raises_error_on_timeout(self):
+ # If the unit does not start before the timeout is reached,
+ # wait_for_unit will raise a RuntimeError.
+ juju_yaml = self._make_juju_status_yaml(
+ unit_state='pending', machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_unit, 'test-service', timeout=0)
+
+ def test_wait_for_relation_returns_if_relation_up(self):
+ # wait_for_relation() waits for relations to come up. If a
+ # relation is already 'up', wait_for_relation() will return
+ # immediately.
+ juju_yaml = self._make_juju_status_yaml(
+ unit_state='started', machine_state='running')
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ charmhelpers.wait_for_relation('test-service', 'db', timeout=0)
+
+ def test_wait_for_relation_times_out_if_relation_not_present(self):
+ # If a relation does not exist at all before a timeout is
+ # reached, wait_for_relation() will raise a RuntimeError.
+ juju_dict = self._make_juju_status_dict(
+ unit_state='started', machine_state='running')
+ units = juju_dict['services']['test-service']['units']
+ # We'll remove all the relations for test-service for this test.
+ units['test-service/0']['relations'] = {}
+ juju_dict['services']['test-service']['units'] = units
+ juju_yaml = yaml.dump(juju_dict)
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_relation, 'test-service',
+ 'db', timeout=0)
+
+ def test_wait_for_relation_times_out_if_relation_not_up(self):
+ # If a relation does not transition to an 'up' state, before a
+ # timeout is reached, wait_for_relation() will raise a
+ # RuntimeError.
+ juju_dict = self._make_juju_status_dict(
+ unit_state='started', machine_state='running')
+ units = juju_dict['services']['test-service']['units']
+ units['test-service/0']['relations']['db']['state'] = 'down'
+ juju_dict['services']['test-service']['units'] = units
+ juju_yaml = yaml.dump(juju_dict)
+ mock_juju_status = lambda: juju_yaml
+ self.patch(charmhelpers, 'juju_status', mock_juju_status)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_relation, 'test-service',
+ 'db', timeout=0)
+
+ def test_wait_for_page_contents_returns_if_contents_available(self):
+ # wait_for_page_contents() will wait until a given string is
+ # contained within the results of a given url and will return
+ # once it does.
+ # We need to patch the charmhelpers instance of urllib2 so that
+ # it doesn't try to connect out.
+ test_content = "Hello, world."
+ new_urlopen = lambda *args: StringIO(test_content)
+ self.patch(charmhelpers.urllib2, 'urlopen', new_urlopen)
+ charmhelpers.wait_for_page_contents(
+ 'http://example.com', test_content, timeout=0)
+
+ def test_wait_for_page_contents_times_out(self):
+ # If the desired contents do not appear within the page before
+ # the specified timeout, wait_for_page_contents() will raise a
+ # RuntimeError.
+ # We need to patch the charmhelpers instance of urllib2 so that
+ # it doesn't try to connect out.
+ new_urlopen = lambda *args: StringIO("This won't work.")
+ self.patch(charmhelpers.urllib2, 'urlopen', new_urlopen)
+ self.assertRaises(
+ RuntimeError, charmhelpers.wait_for_page_contents,
+ 'http://example.com', "This will error", timeout=0)
+
+
+if __name__ == '__main__':
+ unittest.main()
=== added file 'setup.py'
--- setup.py 1970-01-01 00:00:00 +0000
+++ setup.py 2012-04-11 13:26:47 +0000
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU General Public License version 3 (see the file LICENSE).
+
+import ez_setup
+
+
+ez_setup.use_setuptools()
+
+from setuptools import setup, find_packages
+
+__version__ = '0.0.3'
+
+
+setup(
+ name='charmhelpers',
+ version=__version__,
+ packages=find_packages('helpers/python'),
+ package_dir={'': 'helpers/python'},
+ include_package_data=True,
+ zip_safe=False,
+ maintainer='Launchpad Yellow',
+ description=('Helper functions for writing Juju charms'),
+ license='GPL v3',
+ url='https://launchpad.net/charm-tools',
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ ],
+)
Follow ups